@lynn123411/dsh-chat-translate 2.0.0 → 3.0.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
@@ -1,8 +1,10 @@
1
1
  window.__ModuleLoader__.load({ id: "@lynn123411/dsh-chat-translate", factory: (require) => { var module = { exports: {} }; var exports = module.exports;
2
2
  "use strict";
3
+ var __create = Object.create;
3
4
  var __defProp = Object.defineProperty;
4
5
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
6
  var __getOwnPropNames = Object.getOwnPropertyNames;
7
+ var __getProtoOf = Object.getPrototypeOf;
6
8
  var __hasOwnProp = Object.prototype.hasOwnProperty;
7
9
  var __export = (target, all) => {
8
10
  for (var name2 in all)
@@ -16,162 +18,40 @@ var __copyProps = (to, from, except, desc) => {
16
18
  }
17
19
  return to;
18
20
  };
21
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
22
+ // If the importer is in node compatibility mode or this is not an ESM
23
+ // file that has been converted to a CommonJS file using a Babel-
24
+ // compatible transform (i.e. "__esModule" has not been set), then set
25
+ // "default" to the CommonJS "module.exports" for node compatibility.
26
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
27
+ mod
28
+ ));
19
29
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
30
 
21
31
  // src/client/index.ts
22
32
  var index_exports = {};
23
33
  __export(index_exports, {
24
- NonDestructiveTranslationMount: () => NonDestructiveTranslationMount,
34
+ AssistantStepView: () => AssistantStepView,
35
+ ComposerTranslateToggle: () => ComposerTranslateToggle,
36
+ POLICY_BY_MODE: () => POLICY_BY_MODE,
25
37
  apply: () => apply,
26
- chatTranslateObserver: () => chatTranslateObserver,
27
- chunkUnits: () => chunkUnits,
28
- clientCache: () => clientCache,
29
- collectReplyUnits: () => collectReplyUnits,
38
+ chatPresentation: () => chatPresentation,
39
+ chatTranslate: () => chatTranslate,
40
+ createChatPresentation: () => createChatPresentation,
41
+ createTranslateStore: () => createTranslateStore,
42
+ ensureAssistantStyles: () => ensureAssistantStyles,
30
43
  inject: () => inject,
31
- lazyQueue: () => lazyQueue,
32
44
  name: () => name,
33
- needsTranslation: () => needsTranslation,
34
- replyTranslator: () => replyTranslator,
45
+ planAssistantRow: () => planAssistantRow,
35
46
  settingsStore: () => settingsStore,
47
+ setupComposerToggle: () => setupComposerToggle,
36
48
  setupSettingsUi: () => setupSettingsUi
37
49
  });
38
50
  module.exports = __toCommonJS(index_exports);
39
51
 
40
- // src/server/pipeline/mask-tokens.ts
41
- var MASK_TOKEN_PATTERN_SOURCE = "\u27E6([a-z]{4})(\\d+)\u27E7";
42
- var LEGACY_MASK_PATTERN_SOURCE = "_{1,2}DSH\\s*_*\\s*MASKx?\\s*(?:_?\\s*\\d+|_*([a-z]{2,8})\\s*_+\\s*(\\d+))_{0,2}";
43
- function hasMaskResidue(text) {
44
- if (!text) return false;
45
- return new RegExp(MASK_TOKEN_PATTERN_SOURCE, "iu").test(text);
46
- }
47
- function hasLegacyMaskResidue(text) {
48
- if (!text) return false;
49
- return new RegExp(LEGACY_MASK_PATTERN_SOURCE, "iu").test(text);
50
- }
51
- function hasAnyMaskResidue(text) {
52
- return hasMaskResidue(text) || hasLegacyMaskResidue(text);
53
- }
54
-
55
- // src/server/pipeline/masking.ts
56
- function isMaskLeak(translatedText) {
57
- return hasAnyMaskResidue(translatedText);
58
- }
59
-
60
- // src/client/translate/client-cache.ts
61
- var CACHE_KEY = "dsh-chat-translate:cache";
62
- var MAX_ENTRIES = 500;
63
- var RETIRED_CACHE_KEYS = ["dsh-chat-translate:think-cache"];
64
- var TTL_MS = 7 * 24 * 60 * 60 * 1e3;
65
- var ClientCache = class {
66
- // Map preserves insertion order in JS, enabling true LRU semantics.
67
- memCache = /* @__PURE__ */ new Map();
68
- dirty = false;
69
- saveTimer = null;
70
- storageKey;
71
- maxEntries;
72
- constructor(storageKey = CACHE_KEY, maxEntries = MAX_ENTRIES) {
73
- this.storageKey = storageKey;
74
- this.maxEntries = maxEntries;
75
- this.load();
76
- }
77
- load() {
78
- if (typeof localStorage === "undefined") return;
79
- try {
80
- const raw = localStorage.getItem(this.storageKey);
81
- if (raw) {
82
- const obj = JSON.parse(raw);
83
- if (obj && typeof obj === "object") {
84
- for (const [k, entry] of Object.entries(obj)) {
85
- if (typeof entry === "string") {
86
- this.memCache.set(k, { t: 0, v: entry });
87
- } else if (entry && typeof entry === "object" && typeof entry.v === "string") {
88
- this.memCache.set(k, entry);
89
- }
90
- }
91
- }
92
- }
93
- } catch {
94
- }
95
- }
96
- get(text) {
97
- const key = text.trim().toLowerCase();
98
- const entry = this.memCache.get(key);
99
- if (entry === void 0) return void 0;
100
- if (typeof entry.v === "string" && isMaskLeak(entry.v)) {
101
- this.memCache.delete(key);
102
- this.dirty = true;
103
- this.scheduleSave();
104
- return void 0;
105
- }
106
- if (entry.v && entry.v.trim().toLowerCase() === key) {
107
- this.memCache.delete(key);
108
- this.dirty = true;
109
- this.scheduleSave();
110
- return void 0;
111
- }
112
- if (entry.t > 0 && Date.now() - entry.t > TTL_MS) {
113
- this.memCache.delete(key);
114
- this.dirty = true;
115
- this.scheduleSave();
116
- return void 0;
117
- }
118
- this.memCache.delete(key);
119
- this.memCache.set(key, entry);
120
- return entry.v;
121
- }
122
- set(text, translated) {
123
- const key = text.trim().toLowerCase();
124
- if (this.memCache.has(key)) {
125
- this.memCache.delete(key);
126
- } else if (this.memCache.size >= this.maxEntries) {
127
- const oldestKey = this.memCache.keys().next().value;
128
- if (oldestKey !== void 0) {
129
- this.memCache.delete(oldestKey);
130
- }
131
- }
132
- this.memCache.set(key, { t: Date.now(), v: translated });
133
- this.dirty = true;
134
- this.scheduleSave();
135
- }
136
- scheduleSave() {
137
- if (this.saveTimer !== null || typeof window === "undefined") return;
138
- this.saveTimer = window.setTimeout(() => {
139
- this.saveTimer = null;
140
- this.flushSync();
141
- }, 2e3);
142
- }
143
- flushSync() {
144
- if (!this.dirty || typeof localStorage === "undefined") return;
145
- this.dirty = false;
146
- try {
147
- const obj = {};
148
- for (const [k, v] of this.memCache.entries()) {
149
- obj[k] = v;
150
- }
151
- localStorage.setItem(this.storageKey, JSON.stringify(obj));
152
- } catch {
153
- }
154
- }
155
- clear() {
156
- this.memCache.clear();
157
- this.dirty = true;
158
- this.flushSync();
159
- }
160
- size() {
161
- return this.memCache.size;
162
- }
163
- };
164
- function retireClientCaches() {
165
- if (typeof localStorage === "undefined") return;
166
- for (const key of RETIRED_CACHE_KEYS) {
167
- try {
168
- localStorage.removeItem(key);
169
- } catch {
170
- }
171
- }
172
- }
173
- retireClientCaches();
174
- var clientCache = new ClientCache();
52
+ // src/client/settings/ui.tsx
53
+ var import_react2 = require("react");
54
+ var import_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
175
55
 
176
56
  // src/describe-error.ts
177
57
  function describeError(err) {
@@ -244,688 +124,6 @@ async function testServerChannel(channel) {
244
124
  }
245
125
  }
246
126
 
247
- // src/client/translate/mount.ts
248
- var CLASS_ORIGINAL_HIDDEN = "dsh-tidy-original-hidden";
249
- var CLASS_ORIGINAL_SHOWN = "dsh-tidy-original-shown";
250
- var CLASS_TRANSLATED_BLOCK = "dsh-tidy-translated-block";
251
- var CLASS_RUN = "dsh-tidy-run";
252
- var RUN_ATTRIBUTE = "data-tidy-run";
253
- var NonDestructiveTranslationMount = class {
254
- /**
255
- * Mounts a translated string onto the target element non-destructively.
256
- */
257
- static mount(element, translated, options = {}) {
258
- if (!element || !element.ownerDocument) return;
259
- const doc = element.ownerDocument;
260
- let transWrapper = element.querySelector(`:scope > .${CLASS_TRANSLATED_BLOCK}`);
261
- let origWrapper = element.querySelector(
262
- `:scope > .${CLASS_ORIGINAL_HIDDEN}, :scope > .${CLASS_ORIGINAL_SHOWN}`
263
- );
264
- if (transWrapper && origWrapper) {
265
- transWrapper.textContent = translated;
266
- element.dataset.tidyTranslated = "true";
267
- if (options.originalText) element.dataset.original = options.originalText;
268
- return;
269
- }
270
- const originalText = options.originalText ?? this.extractVisibleText(element);
271
- origWrapper = doc.createElement("span");
272
- origWrapper.className = CLASS_ORIGINAL_HIDDEN;
273
- origWrapper.style.display = "none";
274
- while (element.firstChild) {
275
- origWrapper.appendChild(element.firstChild);
276
- }
277
- transWrapper = doc.createElement("span");
278
- transWrapper.className = CLASS_TRANSLATED_BLOCK;
279
- transWrapper.textContent = translated;
280
- const interactive = options.interactive !== false;
281
- if (interactive) {
282
- const showOriginal = (e) => {
283
- e.stopPropagation();
284
- if (!origWrapper || !transWrapper) return;
285
- origWrapper.style.display = "inline";
286
- origWrapper.className = CLASS_ORIGINAL_SHOWN;
287
- transWrapper.style.display = "none";
288
- };
289
- const showTranslated = (e) => {
290
- e.stopPropagation();
291
- if (!origWrapper || !transWrapper) return;
292
- origWrapper.style.display = "none";
293
- origWrapper.className = CLASS_ORIGINAL_HIDDEN;
294
- transWrapper.style.display = "inline";
295
- };
296
- transWrapper.addEventListener("click", showOriginal);
297
- origWrapper.addEventListener("click", showTranslated);
298
- }
299
- element.appendChild(transWrapper);
300
- element.appendChild(origWrapper);
301
- element.dataset.tidyTranslated = "true";
302
- element.dataset.original = originalText;
303
- }
304
- /**
305
- * 把一段连续的行内节点包进自建容器后挂载译文。
306
- *
307
- * 混合块(列表项文字 + 子列表 / 代码块)里的行内片段没有自己的元素,
308
- * 只能先包一层再走同一条非侵入式挂载;嵌套的块级子节点留在容器之外,
309
- * 排版原样保留。
310
- * @returns 自建容器;节点不再相邻或没有共同父节点时返回 null。
311
- */
312
- static mountRun(nodes, translated, options = {}) {
313
- const first = nodes[0];
314
- const parent = first?.parentNode;
315
- if (!first || !parent || !parent.ownerDocument) return null;
316
- if (!nodes.every((node) => node.parentNode === parent)) return null;
317
- const originalText = options.originalText ?? nodes.map((node) => node.textContent ?? "").join("").trim();
318
- if (!originalText) return null;
319
- const run = parent.ownerDocument.createElement("span");
320
- run.className = CLASS_RUN;
321
- run.setAttribute(RUN_ATTRIBUTE, "true");
322
- parent.insertBefore(run, first);
323
- for (const node of nodes) run.appendChild(node);
324
- this.mount(run, translated, { originalText, interactive: options.interactive });
325
- return run;
326
- }
327
- /** 还原行内片段并撤掉自建容器。 */
328
- static unmountRun(run) {
329
- this.unmount(run);
330
- const parent = run.parentNode;
331
- if (!parent) return;
332
- while (run.firstChild) parent.insertBefore(run.firstChild, run);
333
- parent.removeChild(run);
334
- }
335
- /** 还原一个作用域内我们挂过的全部译文(元素单位与行内片段都算)。 */
336
- static restore(scope) {
337
- const mounted = scope.querySelectorAll('[data-tidy-translated="true"]');
338
- for (const element of mounted) {
339
- if (element.getAttribute(RUN_ATTRIBUTE) === "true") this.unmountRun(element);
340
- else this.unmount(element);
341
- }
342
- }
343
- /** 我们自己的挂载容器与还原容器,观察器必须跳过它们。 */
344
- static isOwnNode(element) {
345
- return element.classList?.contains(CLASS_TRANSLATED_BLOCK) === true || element.classList?.contains(CLASS_ORIGINAL_HIDDEN) === true || element.classList?.contains(CLASS_ORIGINAL_SHOWN) === true || element.classList?.contains(CLASS_RUN) === true;
346
- }
347
- /**
348
- * Unmounts translation and restores original DOM nodes completely.
349
- */
350
- static unmount(element) {
351
- if (!element) return;
352
- const origWrapper = element.querySelector(
353
- `:scope > .${CLASS_ORIGINAL_HIDDEN}, :scope > .${CLASS_ORIGINAL_SHOWN}`
354
- );
355
- const transWrapper = element.querySelector(`:scope > .${CLASS_TRANSLATED_BLOCK}`);
356
- if (origWrapper) {
357
- while (origWrapper.firstChild) {
358
- element.insertBefore(origWrapper.firstChild, origWrapper);
359
- }
360
- origWrapper.remove();
361
- }
362
- if (transWrapper) {
363
- transWrapper.remove();
364
- }
365
- if (!origWrapper && element.dataset.original) {
366
- element.textContent = element.dataset.original;
367
- }
368
- delete element.dataset.tidyTranslated;
369
- delete element.dataset.original;
370
- }
371
- /**
372
- * Checks if an element has non-destructive translation mounted.
373
- */
374
- static isMounted(element) {
375
- return element.dataset.tidyTranslated === "true" && !!element.querySelector(`:scope > .${CLASS_TRANSLATED_BLOCK}`);
376
- }
377
- /**
378
- * Gets the original text recorded on the element or contained in origWrapper.
379
- */
380
- static getOriginal(element) {
381
- if (element.dataset.original) return element.dataset.original;
382
- const origWrapper = element.querySelector(
383
- `:scope > .${CLASS_ORIGINAL_HIDDEN}, :scope > .${CLASS_ORIGINAL_SHOWN}`
384
- );
385
- return origWrapper ? origWrapper.textContent?.trim() : void 0;
386
- }
387
- /**
388
- * Extracts text content excluding our own translation wrappers.
389
- */
390
- static extractVisibleText(element) {
391
- const origWrapper = element.querySelector(
392
- `:scope > .${CLASS_ORIGINAL_HIDDEN}, :scope > .${CLASS_ORIGINAL_SHOWN}`
393
- );
394
- if (origWrapper) {
395
- return origWrapper.textContent?.trim() || "";
396
- }
397
- const transWrapper = element.querySelector(`:scope > .${CLASS_TRANSLATED_BLOCK}`);
398
- if (transWrapper) {
399
- return transWrapper.textContent?.trim() || "";
400
- }
401
- return element.textContent?.trim() || "";
402
- }
403
- };
404
-
405
- // src/server/pipeline/blocks.ts
406
- var CJK_CHAR = /[\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff\u3000-\u303f\uff00-\uffef]/;
407
- function estimateTokens(text) {
408
- let cjk = 0;
409
- for (const char of text) {
410
- if (CJK_CHAR.test(char)) cjk += 1;
411
- }
412
- return cjk + Math.ceil((text.length - cjk) / 3);
413
- }
414
-
415
- // src/client/translate/reply.ts
416
- var BLOCK_TAGS = /* @__PURE__ */ new Set([
417
- "P",
418
- "H1",
419
- "H2",
420
- "H3",
421
- "H4",
422
- "H5",
423
- "H6",
424
- "LI",
425
- "BLOCKQUOTE",
426
- "TD",
427
- "TH",
428
- "DT",
429
- "DD",
430
- "FIGCAPTION"
431
- ]);
432
- var CONTAINER_TAGS = /* @__PURE__ */ new Set(["UL", "OL", "DL", "TABLE", "THEAD", "TBODY", "TR", "DIV", "SECTION"]);
433
- var NESTED_BOUNDARY_SELECTOR = [...BLOCK_TAGS, ...CONTAINER_TAGS, "PRE"].join(",").toLowerCase();
434
- var REQUEST_TOKENS = 2048;
435
- var HAS_LATIN = /[A-Za-z]/;
436
- function isMountedElement(element) {
437
- return element.dataset?.tidyTranslated === "true";
438
- }
439
- function hasNestedBoundary(element) {
440
- return element.querySelector(NESTED_BOUNDARY_SELECTOR) !== null;
441
- }
442
- function needsTranslation(text) {
443
- return HAS_LATIN.test(text);
444
- }
445
- function collectReplyUnits(body) {
446
- const units = [];
447
- const pushRun = (nodes) => {
448
- if (nodes.length === 0) return;
449
- const text = nodes.map((node) => node.textContent ?? "").join("").trim();
450
- if (!text || !needsTranslation(text)) return;
451
- const anchor = nodes[0];
452
- if (anchor === void 0) return;
453
- units.push({ nodes, text, anchor });
454
- };
455
- const walk = (container) => {
456
- let run = [];
457
- const flush = () => {
458
- pushRun(run);
459
- run = [];
460
- };
461
- for (const node of Array.from(container.childNodes)) {
462
- if (node.nodeType !== 1) {
463
- run.push(node);
464
- continue;
465
- }
466
- const element = node;
467
- if (NonDestructiveTranslationMount.isOwnNode(element)) {
468
- flush();
469
- continue;
470
- }
471
- if (element.tagName === "PRE") {
472
- flush();
473
- continue;
474
- }
475
- if (BLOCK_TAGS.has(element.tagName)) {
476
- flush();
477
- collectBlock(element);
478
- continue;
479
- }
480
- if (CONTAINER_TAGS.has(element.tagName)) {
481
- flush();
482
- walk(element);
483
- continue;
484
- }
485
- run.push(element);
486
- }
487
- flush();
488
- };
489
- const collectBlock = (element) => {
490
- if (isMountedElement(element)) return;
491
- if (!hasNestedBoundary(element)) {
492
- const text = (element.textContent ?? "").trim();
493
- if (text && needsTranslation(text)) units.push({ element, text, anchor: element });
494
- return;
495
- }
496
- walk(element);
497
- };
498
- walk(body);
499
- return units;
500
- }
501
- function chunkUnits(units, maxTokens = REQUEST_TOKENS) {
502
- const chunks = [];
503
- let current = [];
504
- let tokens = 0;
505
- for (const unit of units) {
506
- const cost = Math.max(estimateTokens(unit.text), 1);
507
- if (current.length > 0 && tokens + cost > maxTokens) {
508
- chunks.push(current);
509
- current = [];
510
- tokens = 0;
511
- }
512
- current.push(unit);
513
- tokens += cost;
514
- }
515
- if (current.length > 0) chunks.push(current);
516
- return chunks;
517
- }
518
- var ReplyTranslateController = class {
519
- /** 正文容器 → 在途 Promise,避免同一条回答被重复翻译。 */
520
- inFlight = /* @__PURE__ */ new WeakMap();
521
- /**
522
- * 翻译一条回答的正文容器。已命中缓存的单位立即挂上,其余按块请求;
523
- * 失败的块保留原文(结果不入缓存,下次滚回视口会自然重试)。
524
- * @param body - 正文容器(markdown body)。
525
- * @returns 本次翻译完成后结算(已在途则返回同一次在途)。
526
- */
527
- translateBody(body) {
528
- const running = this.inFlight.get(body);
529
- if (running !== void 0) return running;
530
- const task = this.run(body).finally(() => {
531
- this.inFlight.delete(body);
532
- });
533
- this.inFlight.set(body, task);
534
- return task;
535
- }
536
- async run(body) {
537
- const units = collectReplyUnits(body);
538
- if (units.length === 0) return;
539
- const missing = [];
540
- for (const unit of units) {
541
- const cached = clientCache.get(unit.text);
542
- if (cached === void 0) missing.push(unit);
543
- else this.applyUnit(unit, cached);
544
- }
545
- if (missing.length === 0) return;
546
- await this.translateUnits(missing);
547
- }
548
- /**
549
- * 逐块请求并挂载。失败的块保持原文:界面上不留提示,控制台留一条警告,结果
550
- * 不进缓存,因此这一块在下次进入视口时会自然重试。
551
- */
552
- async translateUnits(units) {
553
- for (const chunk of chunkUnits(units)) {
554
- const texts = chunk.map((unit) => unit.text);
555
- let results;
556
- try {
557
- results = await requestTranslateReply(texts);
558
- } catch (err) {
559
- console.warn(
560
- `[dsh-chat-translate] \u6B63\u6587\u7FFB\u8BD1\u5757\u8BF7\u6C42\u5931\u8D25\uFF0C\u4FDD\u7559\u539F\u6587: ${err?.message ?? err}`
561
- );
562
- continue;
563
- }
564
- const byText = new Map(results.map((result) => [result.original, result]));
565
- for (const unit of chunk) {
566
- const result = byText.get(unit.text);
567
- if (result === void 0 || !result.ok || !result.translated.trim() || result.translated.trim() === unit.text.trim()) {
568
- console.warn(
569
- `[dsh-chat-translate] \u6B63\u6587\u7FFB\u8BD1\u672A\u6210\u529F\uFF0C\u4FDD\u7559\u539F\u6587: "${unit.text.slice(0, 40)}"`
570
- );
571
- continue;
572
- }
573
- clientCache.set(unit.text, result.translated);
574
- this.applyUnit(unit, result.translated);
575
- }
576
- }
577
- }
578
- /** 把译文挂到单位上;期间文本或节点已经变动的单位直接放弃(结果已进缓存)。 */
579
- applyUnit(unit, translated) {
580
- if (unit.element !== void 0) {
581
- const element = unit.element;
582
- if (!element.isConnected || isMountedElement(element)) return;
583
- if ((element.textContent ?? "").trim() !== unit.text) return;
584
- NonDestructiveTranslationMount.mount(element, translated, {
585
- originalText: unit.text
586
- });
587
- return;
588
- }
589
- const nodes = unit.nodes ?? [];
590
- if (nodes.length === 0 || nodes.some((node) => !node.isConnected)) return;
591
- const parent = nodes[0]?.parentNode ?? null;
592
- if (parent === null || !nodes.every((node) => node.parentNode === parent)) return;
593
- const text = nodes.map((node) => node.textContent ?? "").join("").trim();
594
- if (text !== unit.text) return;
595
- NonDestructiveTranslationMount.mountRun(nodes, translated, {
596
- originalText: unit.text
597
- });
598
- }
599
- };
600
- var replyTranslator = new ReplyTranslateController();
601
-
602
- // src/client/translate/lazy.ts
603
- var VIEWPORT_MARGIN = "150px 0px";
604
- var SETTLE_DEBOUNCE_MS = 120;
605
- var LazyTranslationQueue = class {
606
- enabled = true;
607
- observer = null;
608
- pending = /* @__PURE__ */ new Map();
609
- setEnabled(enabled) {
610
- this.enabled = enabled;
611
- if (!enabled) {
612
- this.disconnect();
613
- }
614
- }
615
- observe(body, immediate = false) {
616
- if (!this.enabled || !body.isConnected) return;
617
- if (immediate || typeof IntersectionObserver === "undefined") {
618
- void replyTranslator.translateBody(body);
619
- return;
620
- }
621
- const observer = this.intersectionObserver();
622
- if (observer === null) {
623
- void replyTranslator.translateBody(body);
624
- return;
625
- }
626
- observer.observe(body);
627
- }
628
- intersectionObserver() {
629
- if (typeof window === "undefined") return null;
630
- if (this.observer !== null) return this.observer;
631
- this.observer = new IntersectionObserver(
632
- (entries) => {
633
- for (const entry of entries) {
634
- if (!entry.isIntersecting) continue;
635
- const body = entry.target;
636
- if (!(body instanceof HTMLElement)) continue;
637
- this.observer?.unobserve(body);
638
- this.scheduleTranslation(body);
639
- }
640
- },
641
- { root: null, rootMargin: VIEWPORT_MARGIN, threshold: 0 }
642
- );
643
- return this.observer;
644
- }
645
- /**
646
- * 进入视口后稍等一拍再翻,避开「刚出现、还在布局」的那一瞬。已经在等的那条
647
- * 不重复排队。
648
- */
649
- scheduleTranslation(body) {
650
- if (typeof window === "undefined") {
651
- void replyTranslator.translateBody(body);
652
- return;
653
- }
654
- if (this.pending.has(body)) return;
655
- const timer = window.setTimeout(() => {
656
- this.pending.delete(body);
657
- if (!this.enabled || !body.isConnected) return;
658
- void replyTranslator.translateBody(body);
659
- }, SETTLE_DEBOUNCE_MS);
660
- this.pending.set(body, timer);
661
- }
662
- disconnect() {
663
- if (typeof window !== "undefined") {
664
- for (const timer of this.pending.values()) window.clearTimeout(timer);
665
- }
666
- this.pending.clear();
667
- if (this.observer) {
668
- this.observer.disconnect();
669
- this.observer = null;
670
- }
671
- }
672
- };
673
- var lazyQueue = new LazyTranslationQueue();
674
-
675
- // src/client/translate/observer.ts
676
- var REPLY_ROW_SELECTOR = '[data-chat-flow-kind][data-chat-group-part="response"]';
677
- var MARKDOWN_BODY_SELECTOR = '[class*="body" i]';
678
- var PROCESS_MEMBER_SELECTOR = "[data-turn-process-member]";
679
- var PROCESS_ANSWER_SELECTOR = "[data-turn-process-answer]";
680
- var EXCLUDED_SELECTOR = [
681
- '[data-variant="think"]',
682
- "[data-chat-call-id]",
683
- '[data-slot="tool.call.toolview"]',
684
- '[data-chat-group-part="reasoning"]'
685
- ].join(",");
686
- var SESSION_ROOT_SELECTOR = "[data-conversation-scroll], [data-chat-flow]";
687
- var ROOT_CHECK_INTERVAL_MS = 3e3;
688
- function isStreaming(root) {
689
- return root.hasAttribute("data-streaming");
690
- }
691
- function isInsideProcessDisclosure(element) {
692
- const row = element.closest(PROCESS_MEMBER_SELECTOR);
693
- if (row === null || !row.contains(element)) return false;
694
- if (row.matches(PROCESS_ANSWER_SELECTOR)) return false;
695
- if (row.querySelector(PROCESS_ANSWER_SELECTOR) !== null) return false;
696
- return true;
697
- }
698
- var ChatTranslateObserver = class {
699
- observer = null;
700
- rootElement = null;
701
- rootCheckTimer = null;
702
- isEnabled = true;
703
- /**
704
- * 已经交给翻译器的正文容器,避免同一条回答在每次 DOM 变动时重扫。总开关重开
705
- * 时清空:关着的那段时间落定的回答需要补翻。
706
- */
707
- handled = /* @__PURE__ */ new WeakSet();
708
- constructor() {
709
- this.handleMutations = this.handleMutations.bind(this);
710
- }
711
- setEnabled(enabled) {
712
- if (enabled === this.isEnabled) return;
713
- this.isEnabled = enabled;
714
- if (enabled) {
715
- lazyQueue.setEnabled(true);
716
- this.handled = /* @__PURE__ */ new WeakSet();
717
- this.observer?.disconnect();
718
- this.observer = null;
719
- this.start();
720
- if (this.rootElement !== null) this.scanContainer(this.rootElement);
721
- } else {
722
- this.restoreOriginals();
723
- this.disconnect();
724
- lazyQueue.setEnabled(false);
725
- }
726
- }
727
- restoreOriginals() {
728
- const scope = this.rootElement ?? document;
729
- NonDestructiveTranslationMount.restore(scope);
730
- this.handled = /* @__PURE__ */ new WeakSet();
731
- }
732
- /**
733
- * The active session's scroll container. Only the currently-viewed session
734
- * is translated; switching sessions replaces this subtree and the observer
735
- * naturally follows the new content.
736
- */
737
- isVisible(el) {
738
- if (el.hasAttribute("hidden")) return false;
739
- if (el.style.display === "none") return false;
740
- try {
741
- return el.getClientRects().length > 0;
742
- } catch {
743
- return true;
744
- }
745
- }
746
- findRoot(documentRef) {
747
- if (!documentRef.body) {
748
- return documentRef.documentElement;
749
- }
750
- const candidates = documentRef.querySelectorAll(SESSION_ROOT_SELECTOR);
751
- for (const el of candidates) {
752
- if (this.isVisible(el)) {
753
- return el;
754
- }
755
- }
756
- return documentRef.body;
757
- }
758
- start(documentRef = document) {
759
- if (!this.isEnabled || typeof window === "undefined") {
760
- return () => {
761
- };
762
- }
763
- const findAndObserveRoot = () => {
764
- const root = this.findRoot(documentRef);
765
- this.rootElement = root;
766
- this.scanContainer(root);
767
- if (!this.observer) {
768
- this.observer = new MutationObserver(this.handleMutations);
769
- this.observer.observe(root, {
770
- childList: true,
771
- subtree: true,
772
- attributes: true,
773
- characterData: true,
774
- // `data-streaming` 从 markdown 根上消失就是落定信号;`data-turn-process-*`
775
- // 是内核改判「哪些 flow item 属于折叠块」时动的属性,它一变就要重扫。
776
- attributeFilter: [
777
- "data-streaming",
778
- "data-chat-flow-kind",
779
- "data-chat-group-part",
780
- "data-turn-process-member",
781
- "data-turn-process-answer"
782
- ]
783
- });
784
- }
785
- };
786
- findAndObserveRoot();
787
- this.scheduleRootCheck();
788
- return () => {
789
- this.disconnect();
790
- };
791
- }
792
- scheduleRootCheck() {
793
- if (this.rootCheckTimer !== null || typeof window === "undefined") return;
794
- this.rootCheckTimer = window.setInterval(() => {
795
- if (this.rootElement && this.rootElement.isConnected && this.isVisible(this.rootElement)) {
796
- return;
797
- }
798
- this.restart();
799
- }, ROOT_CHECK_INTERVAL_MS);
800
- }
801
- restart() {
802
- if (typeof window === "undefined") return;
803
- const wasEnabled = this.isEnabled;
804
- this.disconnect();
805
- if (wasEnabled) {
806
- this.start();
807
- }
808
- }
809
- handleMutations(mutations) {
810
- if (!this.isEnabled) return;
811
- for (const mutation of mutations) {
812
- if (mutation.type === "childList") {
813
- let addedElement = false;
814
- for (let i = 0; i < mutation.addedNodes.length; i++) {
815
- const node = mutation.addedNodes[i];
816
- if (node instanceof HTMLElement) {
817
- if (NonDestructiveTranslationMount.isOwnNode(node)) continue;
818
- addedElement = true;
819
- this.scanNode(node);
820
- }
821
- }
822
- const target = mutation.target;
823
- if (target instanceof HTMLElement) {
824
- if (!addedElement && !NonDestructiveTranslationMount.isOwnNode(target)) {
825
- this.scanNode(target);
826
- }
827
- }
828
- } else if (mutation.type === "attributes") {
829
- const target = mutation.target;
830
- if (target instanceof HTMLElement) {
831
- if (NonDestructiveTranslationMount.isOwnNode(target)) continue;
832
- this.scanNode(target);
833
- }
834
- } else if (mutation.type === "characterData") {
835
- const parent = mutation.target.parentElement;
836
- if (parent instanceof HTMLElement) {
837
- if (NonDestructiveTranslationMount.isOwnNode(parent)) continue;
838
- this.scanNode(parent);
839
- }
840
- }
841
- }
842
- }
843
- /**
844
- * 扫一棵子树,把里面**全部**该翻的正文容器交给翻译器。
845
- *
846
- * 不再「命中第一个就返回」:一个容器里可以有好几行 response(折叠块里每一步
847
- * 露出的一段正文就是一行),只取第一个会把后面那些整片漏掉。
848
- */
849
- scanContainer(container) {
850
- this.tryBodies(container);
851
- if (container === null) return;
852
- for (const row of container.querySelectorAll(REPLY_ROW_SELECTOR)) {
853
- this.tryRow(row);
854
- }
855
- }
856
- /** 扫一个刚变动过的节点(含它所在的这一行,以及它内部的每一行)。 */
857
- scanNode(node) {
858
- this.tryBodies(node);
859
- const row = node.closest(REPLY_ROW_SELECTOR);
860
- if (row !== null && row !== node) this.tryRow(row);
861
- for (const child of node.querySelectorAll(REPLY_ROW_SELECTOR)) {
862
- this.tryRow(child);
863
- }
864
- }
865
- /** 一行 flow item 里的**每一个**正文容器;工具行、Think 卡会被排除规则挡掉。 */
866
- tryRow(row) {
867
- this.tryBodies(row);
868
- if (row === null) return;
869
- for (const body of row.querySelectorAll(MARKDOWN_BODY_SELECTOR)) {
870
- this.tryBody(body);
871
- }
872
- }
873
- /** 判定并交给翻译器:这个元素自己或它内部的每一条落定正文容器。 */
874
- tryBodies(element) {
875
- this.tryBody(element);
876
- if (element === null) return;
877
- for (const body of element.querySelectorAll(MARKDOWN_BODY_SELECTOR)) {
878
- this.tryBody(body);
879
- }
880
- }
881
- /** 判定并交给翻译器:这一个元素自己是不是一条该翻的正文容器。 */
882
- tryBody(element) {
883
- if (!(element instanceof HTMLElement)) return;
884
- if (!this.isTranslatableBody(element)) return;
885
- this.requestTranslate(element);
886
- }
887
- /**
888
- * 这个元素自己是不是一条该翻的正文容器:落定的、落在过程折叠块里的 markdown
889
- * 正文,且不被排除规则挡住(Think 卡、工具行、最终汇总都不算)。
890
- */
891
- isTranslatableBody(element) {
892
- if (!(element instanceof HTMLElement)) return false;
893
- if (!element.matches(MARKDOWN_BODY_SELECTOR)) return false;
894
- if (element.closest(EXCLUDED_SELECTOR) !== null) return false;
895
- if (element.closest(REPLY_ROW_SELECTOR) === null) return false;
896
- if (!isInsideProcessDisclosure(element)) return false;
897
- const root = element.closest("[data-streaming]") ?? element;
898
- if (isStreaming(root)) return false;
899
- return true;
900
- }
901
- /**
902
- * 把一条落定的正文交给视口懒队列:视口内立刻翻、视口外的等滚到再翻,
903
- * 命中缓存的由翻译器立即挂上。同一条回答只登记一次。
904
- */
905
- requestTranslate(body) {
906
- if (this.handled.has(body)) return;
907
- this.handled.add(body);
908
- lazyQueue.observe(body);
909
- }
910
- disconnect() {
911
- if (this.rootCheckTimer !== null) {
912
- clearInterval(this.rootCheckTimer);
913
- this.rootCheckTimer = null;
914
- }
915
- if (this.observer) {
916
- this.observer.disconnect();
917
- this.observer = null;
918
- }
919
- lazyQueue.disconnect();
920
- this.rootElement = null;
921
- }
922
- };
923
- var chatTranslateObserver = new ChatTranslateObserver();
924
-
925
- // src/client/settings/ui.tsx
926
- var import_react2 = require("react");
927
- var import_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
928
-
929
127
  // src/client/settings/store.ts
930
128
  var AI_TIMEOUT_MIN = 500;
931
129
  var AI_TIMEOUT_MAX = 9e5;
@@ -994,14 +192,7 @@ var SettingsStore = class {
994
192
  }
995
193
  applyState(next) {
996
194
  next.aiConfigured = Boolean(next.baseUrl && next.model && this.keyConfigured);
997
- const enabledChanged = next.enabled !== this.state.enabled;
998
195
  this.state = next;
999
- if (enabledChanged) {
1000
- try {
1001
- chatTranslateObserver.setEnabled(this.state.enabled);
1002
- } catch {
1003
- }
1004
- }
1005
196
  this.notify();
1006
197
  }
1007
198
  notify() {
@@ -1354,7 +545,7 @@ var zh = {
1354
545
  title: "\u6B63\u6587\u7FFB\u8BD1",
1355
546
  masterTitle: "\u7FFB\u8BD1\u603B\u5F00\u5173",
1356
547
  enableTranslation: "\u542F\u7528\u7FFB\u8BD1",
1357
- masterDesc: "\u81EA\u52A8\u628A\u56DE\u7B54\u6B63\u6587\u91CC\u9700\u8981\u9605\u8BFB\u7684\u82F1\u6587\u7FFB\u6210\u4E2D\u6587\uFF08\u5982 {example}\uFF09\uFF0C\u601D\u8003\u5361\u4E0E\u5DE5\u5177\u8C03\u7528\u884C\u4E00\u5F8B\u4E0D\u7FFB\uFF0C\u4EE3\u7801\u5757\u539F\u6837\u4FDD\u7559\u3002\u8BD1\u6587\u6302\u5728\u539F\u6587\u4F4D\u7F6E\u4E0A\uFF0C\u70B9\u51FB\u8BD1\u6587\u53EF\u539F\u5730\u5207\u56DE\u539F\u6587\u3002\u56DE\u7B54\u8FD8\u5728\u6D41\u5F0F\u8F93\u51FA\u65F6\u4E0D\u7FFB\uFF0C\u843D\u5B9A\u540E\u6574\u6761\u8F6C\u4E3A\u4E2D\u6587\uFF1B\u5DF2\u7ECF\u5199\u597D\u7684\u4E2D\u6587\u6BB5\u843D\u539F\u6837\u4FDD\u7559\u3002\u53EA\u4F5C\u7528\u4E8E\u5F53\u524D\u67E5\u770B\u7684\u4F1A\u8BDD\uFF0C\u8BD1\u6587\u4E0D\u5199\u56DE\u4F1A\u8BDD\u4E0A\u4E0B\u6587\u3002",
548
+ masterDesc: "\u81EA\u52A8\u628A\u52A9\u624B\u56DE\u7B54\u6B63\u6587\u7FFB\u6210\u4E2D\u6587\uFF08\u5982 {example}\uFF09\uFF1A\u6298\u53E0\u5757\u91CC\u7684\u8FC7\u7A0B\u6B63\u6587\u4E0E\u6700\u7EC8\u6C47\u603B\u4E00\u89C6\u540C\u4EC1\uFF0C\u6574\u6BB5\u4E2D\u6587\u7684\u5757\u4E5F\u7167\u9001\uFF0C\u7531\u6A21\u578B\u4E00\u5E76\u6539\u5199\u6210\u81EA\u7136\u4E2D\u6587\u3001\u987A\u6389\u673A\u5668\u8154\u3002Think \u5361\u4E0E\u5DE5\u5177\u8C03\u7528\u884C\u4E0D\u7FFB\uFF0C\u4EE3\u7801\u5757\u539F\u6837\u4FDD\u7559\u3002\u7FFB\u8BD1\u8FC7\u7684\u5757\u5DE6\u7F18\u6709\u4E00\u6761\u7AD6\u7EBF\uFF1A\u663E\u793A\u8BD1\u6587\u65F6\u662F\u7A0D\u7C97\u7684\u84DD\u7EBF\uFF0C\u70B9\u56DE\u539F\u6587\u53D8\u7EC6\u7070\u7EBF\uFF08\u63D0\u793A\u8FD9\u91CC\u5907\u7740\u8BD1\u6587\uFF0C\u518D\u70B9\u5373\u5207\u56DE\uFF09\uFF0C\u6CA1\u8BD1\u6210\u7684\u6CA1\u6709\u7EBF\u2014\u2014\u7EBF\u7684\u989C\u8272\u4E0E\u7C97\u7EC6\u4E00\u773C\u5206\u6E05\u300C\u6709\u6CA1\u6709\u8BD1\u300D\u548C\u300C\u6B63\u5728\u8BFB\u54EA\u4E00\u7248\u300D\u3002\u70B9\u51FB\u5DF2\u7FFB\u8BD1\u7684\u5757\u53EF\u5728\u8BD1\u6587\u4E0E\u539F\u6587\u95F4\u5207\u6362\uFF0C\u6A21\u578B\u8BA4\u4E3A\u539F\u6837\u6700\u597D\u3001\u8FD4\u56DE\u4E00\u6A21\u4E00\u6837\u7684\u6587\u5B57\u65F6\u540C\u6837\u7B97\u7FFB\u8BD1\u8FC7\u3001\u540C\u6837\u53EF\u5207\u3002\u8BD1\u6587\u6309\u539F markdown \u683C\u5F0F\u91CD\u65B0\u6E32\u67D3\uFF0C\u8868\u683C\u3001\u5217\u8868\u3001\u52A0\u7C97\u4E0E\u884C\u5185\u4EE3\u7801\u4FDD\u6301\u7ED3\u6784\u3002\u56DE\u7B54\u8FD8\u5728\u6D41\u5F0F\u8F93\u51FA\u65F6\u4E0D\u7FFB\uFF0C\u843D\u5B9A\u540E\u6309\u9605\u8BFB\u987A\u5E8F\u9010\u6BB5\u51FA\u4E2D\u6587\u3002\u53EA\u4F5C\u7528\u4E8E\u5F53\u524D\u67E5\u770B\u7684\u4F1A\u8BDD\uFF0C\u8BD1\u6587\u4E0D\u5199\u56DE\u4F1A\u8BDD\u4E0A\u4E0B\u6587\u3002\u8F93\u5165\u6846\u4E0B\u65B9\u90A3\u4E00\u884C\u6709\u540C\u6B3E\u5F00\u5173\uFF0C\u4E24\u5904\u72B6\u6001\u540C\u6B65\u3002",
1358
549
  aiTitle: "AI \u7FFB\u8BD1\uFF08OpenAI \u517C\u5BB9\u534F\u8BAE\uFF09",
1359
550
  aiRule: "\u53EA\u6709\u8FD9\u4E00\u6761\u901A\u9053\uFF1AKey / Base URL / \u6A21\u578B\u9F50\u5907\u624D\u4F1A\u7FFB\u8BD1\uFF0C\u7F3A\u4EFB\u4F55\u4E00\u9879\u90FD\u4FDD\u6301\u539F\u6587\u3002",
1360
551
  badgeConfigured: "\u5DF2\u914D\u7F6E",
@@ -1379,14 +570,17 @@ var zh = {
1379
570
  testOk: "\u8FDE\u63A5\u6B63\u5E38\uFF0C\u5EF6\u8FDF {latency}ms",
1380
571
  testFail: "\u5931\u8D25\uFF1A{error}",
1381
572
  behaviorTitle: "\u7FFB\u8BD1\u8303\u56F4",
1382
- behaviorRule: "\u9700\u8981\u9605\u8BFB\u7684\u56DE\u7B54\u6B63\u6587\uFF1B\u601D\u8003\u94FE\u6B63\u6587\u3001\u5DE5\u5177\u8C03\u7528\u6807\u9898\u4E0E\u6298\u53E0\u6458\u8981\u6C38\u4E0D\u7FFB\u8BD1\uFF0C\u4EE3\u7801\u5757\u53CA\u5176\u5185\u90E8\u539F\u6837\u4FDD\u7559\u3002"
573
+ behaviorRule: "\u52A9\u624B\u56DE\u7B54\u6B63\u6587\u4E00\u5F8B\u7FFB\u8BD1\uFF0C\u5305\u62EC\u6700\u7EC8\u6C47\u603B\u4E0E\u6574\u6BB5\u4E2D\u6587\u7684\u5757\uFF1A\u4E2D\u6587\u5757\u540C\u6837\u4EA4\u7ED9\u6A21\u578B\u6539\u5199\u6210\u81EA\u7136\u4E2D\u6587\uFF0C\u610F\u4E49\u3001\u672F\u8BED\u4E0E\u6570\u5B57\u4E0D\u53D8\u3002\u601D\u8003\u94FE\u6B63\u6587\u3001\u5DE5\u5177\u8C03\u7528\u6807\u9898\u4E0E\u6298\u53E0\u6458\u8981\u4E0D\u7FFB\u8BD1\uFF0C\u4EE3\u7801\u5757\u53CA\u5176\u5185\u90E8\u539F\u6837\u4FDD\u7559\u3002",
574
+ dockLabel: "\u8BD1",
575
+ dockOn: "\u6B63\u6587\u7FFB\u8BD1\u5DF2\u5F00\u542F\uFF0C\u70B9\u51FB\u5173\u95ED",
576
+ dockOff: "\u6B63\u6587\u7FFB\u8BD1\u5DF2\u5173\u95ED\uFF0C\u70B9\u51FB\u5F00\u542F"
1383
577
  };
1384
578
  var en = {
1385
579
  pageNav: "Reading",
1386
580
  title: "Reply translate",
1387
581
  masterTitle: "Translation master switch",
1388
582
  enableTranslation: "Enable translation",
1389
- masterDesc: "Translates the English prose in assistant replies (e.g. {example}) into Chinese. Think cards and tool-call rows are never translated, and code blocks stay untouched. The translation is mounted in place of the original and clicking it switches back to the original in place. A reply that is still streaming is left alone until it settles, and paragraphs already written in Chinese pass through unchanged. Applies to the session you are viewing only; translations never enter the conversation context.",
583
+ masterDesc: "Translates assistant reply prose (e.g. {example}) into Chinese: text inside disclosure blocks and the final summary are treated alike, and blocks written entirely in Chinese go through the model too, which rewrites them into natural Chinese and smooths out machine-flavored phrasing. Think cards and tool-call rows are never translated, and code blocks stay untouched. A translated block carries a left edge line: a slightly thicker blue one while its translation shows, a thin grey one while you read the original (a translation is waiting one click away), and none at all when nothing translated \u2014 colour and weight together tell at a glance both what exists and what you are reading. Click any translated block to switch between translation and original; even when the model decides the original wording is already best and returns it unchanged, it still counts as translated and stays switchable. Translations are re-rendered as Markdown, so tables, lists, bold runs and inline code keep their structure. A reply that is still streaming is left alone until it settles, then prose turns to Chinese paragraph by paragraph in reading order. Applies to the session you are viewing only; translations never enter the conversation context. The same switch sits on the row below the composer, sharing its state.",
1390
584
  aiTitle: "AI translation (OpenAI-compatible)",
1391
585
  aiRule: "This is the only channel: translation runs when Key, Base URL and model are all present; missing any one leaves the text as it is.",
1392
586
  badgeConfigured: "Configured",
@@ -1411,7 +605,10 @@ var en = {
1411
605
  testOk: "Connected, {latency}ms latency",
1412
606
  testFail: "Failed: {error}",
1413
607
  behaviorTitle: "What gets translated",
1414
- behaviorRule: "The assistant reply prose you have to read. Think-chain text, tool-call titles and folded summaries are never translated; code blocks and their contents stay untouched."
608
+ behaviorRule: "All assistant reply prose, the final summary and Chinese-only blocks included: Chinese blocks are also handed to the model, which rewrites them into natural Chinese while keeping meaning, terms and numbers unchanged. Think-chain text, tool-call titles and folded summaries are never translated; code blocks and their contents stay untouched.",
609
+ dockLabel: "Translate",
610
+ dockOn: "Translation is on; click to turn it off",
611
+ dockOff: "Translation is off; click to turn it on"
1415
612
  };
1416
613
 
1417
614
  // src/client/settings/ui.tsx
@@ -1666,12 +863,791 @@ function setupSettingsUi(ctx) {
1666
863
  }
1667
864
  }
1668
865
 
866
+ // src/client/composer/dock-toggle.tsx
867
+ var import_react3 = require("react");
868
+ var import_dsh_client_ui_primitives2 = require("@deepseek-ai/dsh-client-ui-primitives");
869
+ var import_jsx_runtime2 = require("react/jsx-runtime");
870
+ function ComposerTranslateToggle(props) {
871
+ const { t } = props;
872
+ const [state, setState] = (0, import_react3.useState)(() => settingsStore.getState());
873
+ (0, import_react3.useEffect)(() => settingsStore.subscribe(() => setState(settingsStore.getState())), []);
874
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_dsh_client_ui_primitives2.Tooltip, { label: () => state.enabled ? t("dockOn") : t("dockOff"), side: "top", align: "end", portal: true, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
875
+ import_dsh_client_ui_primitives2.Pill,
876
+ {
877
+ active: state.enabled,
878
+ onClick: () => void settingsStore.update({ enabled: !settingsStore.getState().enabled }),
879
+ children: t("dockLabel")
880
+ }
881
+ ) });
882
+ }
883
+ function setupComposerToggle(ctx) {
884
+ if (typeof window === "undefined") return () => {
885
+ };
886
+ const slots = ctx?.slots;
887
+ if (!slots || typeof slots.inject !== "function") return () => {
888
+ };
889
+ return slots.inject(
890
+ "conversation.composer.dock",
891
+ () => slots.register(
892
+ {
893
+ name: "conversation.composer.dock",
894
+ id: "dsh-chat-translate-toggle",
895
+ // -2 排在 dsh-a6api 胶囊(-1)的左侧,整行最左;官方统计胶囊是 0。
896
+ order: -2,
897
+ locale: NS
898
+ },
899
+ ComposerTranslateToggle
900
+ )
901
+ );
902
+ }
903
+
904
+ // src/client/chat/assistant-step.tsx
905
+ var React = __toESM(require("react"), 1);
906
+ var import_react4 = require("react");
907
+ var import_dsh_client_ui_primitives3 = require("@deepseek-ai/dsh-client-ui-primitives");
908
+
909
+ // src/server/pipeline/blocks.ts
910
+ var REPLY_MAX_INPUT_TOKENS = 4096;
911
+ var CJK_CHAR = /[\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff\u3000-\u303f\uff00-\uffef]/;
912
+ function estimateTokens(text) {
913
+ let cjk = 0;
914
+ for (const char of text) {
915
+ if (CJK_CHAR.test(char)) cjk += 1;
916
+ }
917
+ return cjk + Math.ceil((text.length - cjk) / 3);
918
+ }
919
+
920
+ // src/client/chat/translate-store.ts
921
+ var MAX_TRANSLATED_ROWS = 200;
922
+ var MAX_ROW_ATTEMPTS = 3;
923
+ var REQUEST_TOKEN_BUDGET = REPLY_MAX_INPUT_TOKENS;
924
+ function chunkTexts(texts) {
925
+ const batches = [];
926
+ let current = [];
927
+ let tokens = 0;
928
+ for (const text of texts) {
929
+ const cost = Math.max(estimateTokens(text), 1);
930
+ if (current.length > 0 && tokens + cost > REQUEST_TOKEN_BUDGET) {
931
+ batches.push(current);
932
+ current = [];
933
+ tokens = 0;
934
+ }
935
+ current.push(text);
936
+ tokens += cost;
937
+ }
938
+ if (current.length > 0) batches.push(current);
939
+ return batches;
940
+ }
941
+ var ChatTranslateStore = class {
942
+ rows = /* @__PURE__ */ new Map();
943
+ listeners = /* @__PURE__ */ new Set();
944
+ version = 0;
945
+ fetch;
946
+ constructor(fetcher) {
947
+ this.fetch = fetcher;
948
+ }
949
+ /** 单调版本号:任何状态变化(含单批落定)都前进一位,供渲染层判等重渲。 */
950
+ getVersion() {
951
+ return this.version;
952
+ }
953
+ subscribe = (listener) => {
954
+ this.listeners.add(listener);
955
+ return () => {
956
+ this.listeners.delete(listener);
957
+ };
958
+ };
959
+ /** 读一行状态,并把该行刷成最近使用。 */
960
+ getState(rowKey) {
961
+ const state = this.rows.get(rowKey);
962
+ if (state === void 0) return void 0;
963
+ this.rows.delete(rowKey);
964
+ this.rows.set(rowKey, state);
965
+ return state;
966
+ }
967
+ /**
968
+ * 确保该行按当前文本被翻译。
969
+ * @returns 是否发出了新的请求(同代 done/pending 与换代前的登记之外都是 false)。
970
+ */
971
+ ensure(rowKey, texts) {
972
+ const current = this.rows.get(rowKey);
973
+ let attempts = 1;
974
+ if (current !== void 0 && sameTexts(current.texts, texts)) {
975
+ if (current.status !== "partial") return false;
976
+ if (current.attempts >= MAX_ROW_ATTEMPTS) return false;
977
+ attempts = current.attempts + 1;
978
+ }
979
+ const snapshot = [...texts];
980
+ const next = {
981
+ status: "pending",
982
+ texts: snapshot,
983
+ outcomes: snapshot.map(() => null),
984
+ attempts
985
+ };
986
+ this.rows.delete(rowKey);
987
+ this.rows.set(rowKey, next);
988
+ this.prune();
989
+ this.bump();
990
+ if (snapshot.length > 0) void this.run(rowKey, snapshot);
991
+ return true;
992
+ }
993
+ async run(rowKey, texts) {
994
+ const outcomes = texts.map(() => null);
995
+ let offset = 0;
996
+ for (const batch of chunkTexts(texts)) {
997
+ let results = [];
998
+ try {
999
+ results = await this.fetch(batch);
1000
+ } catch {
1001
+ results = [];
1002
+ }
1003
+ for (let i = 0; i < batch.length; i++) {
1004
+ const result = results[i];
1005
+ if (result !== void 0 && result.ok && result.translated && result.translated.trim()) {
1006
+ outcomes[offset + i] = { translated: result.translated, ok: true };
1007
+ } else {
1008
+ outcomes[offset + i] = { translated: batch[i] ?? texts[offset + i] ?? "", ok: false };
1009
+ }
1010
+ }
1011
+ offset += batch.length;
1012
+ const live = this.rows.get(rowKey);
1013
+ if (live === void 0 || live.status !== "pending" || !sameTexts(live.texts, texts)) return;
1014
+ this.rows.set(rowKey, {
1015
+ status: "pending",
1016
+ texts: live.texts,
1017
+ outcomes: [...outcomes],
1018
+ attempts: live.attempts
1019
+ });
1020
+ this.bump();
1021
+ }
1022
+ const state = this.rows.get(rowKey);
1023
+ if (state?.status === "pending" && sameTexts(state.texts, texts)) {
1024
+ const anyFailed = outcomes.some((outcome) => outcome === null || !outcome.ok);
1025
+ this.rows.set(rowKey, {
1026
+ status: anyFailed ? "partial" : "done",
1027
+ texts: state.texts,
1028
+ outcomes,
1029
+ attempts: state.attempts
1030
+ });
1031
+ this.bump();
1032
+ }
1033
+ }
1034
+ prune() {
1035
+ while (this.rows.size > MAX_TRANSLATED_ROWS) {
1036
+ const oldest = this.rows.keys().next().value;
1037
+ if (oldest === void 0) break;
1038
+ this.rows.delete(oldest);
1039
+ }
1040
+ }
1041
+ bump() {
1042
+ this.version += 1;
1043
+ for (const listener of [...this.listeners]) {
1044
+ try {
1045
+ listener();
1046
+ } catch {
1047
+ }
1048
+ }
1049
+ }
1050
+ };
1051
+ function sameTexts(a, b) {
1052
+ if (a === b) return true;
1053
+ if (a.length !== b.length) return false;
1054
+ for (let i = 0; i < a.length; i++) {
1055
+ if (a[i] !== b[i]) return false;
1056
+ }
1057
+ return true;
1058
+ }
1059
+ function createTranslateStore(fetcher) {
1060
+ return new ChatTranslateStore(fetcher);
1061
+ }
1062
+ var chatTranslate = createTranslateStore(requestTranslateReply);
1063
+
1064
+ // src/client/chat/row-plan.ts
1065
+ function planAssistantRow(input) {
1066
+ const { blocks, streaming, interrupted, groupPart, canTranslate, outcomes } = input;
1067
+ const hasVisible = streaming || interrupted || blocks.some((block) => block.kind !== "tool-call");
1068
+ if (!hasVisible) return { entries: null, texts: [] };
1069
+ const texts = [];
1070
+ if (groupPart !== "reasoning") {
1071
+ for (const block of blocks) {
1072
+ if (block.kind === "text") texts.push(block.text ?? "");
1073
+ }
1074
+ }
1075
+ const entries = [];
1076
+ let textIndex = 0;
1077
+ let imageRun = null;
1078
+ const flushImages = () => {
1079
+ if (imageRun !== null) {
1080
+ entries.push({ type: "images", key: entries.length, attachments: imageRun });
1081
+ imageRun = null;
1082
+ }
1083
+ };
1084
+ for (let i = 0; i < blocks.length; i++) {
1085
+ const block = blocks[i];
1086
+ if (block === void 0) continue;
1087
+ if (groupPart === "reasoning" && block.kind !== "reasoning") continue;
1088
+ if (groupPart === "response" && block.kind === "reasoning") continue;
1089
+ switch (block.kind) {
1090
+ case "text": {
1091
+ flushImages();
1092
+ const index = textIndex;
1093
+ textIndex += 1;
1094
+ const outcome = canTranslate && !streaming && outcomes !== void 0 ? outcomes[index] : null;
1095
+ entries.push({
1096
+ type: "prose",
1097
+ key: entries.length,
1098
+ text: block.text ?? "",
1099
+ translated: outcome !== null && outcome !== void 0 && outcome.ok ? outcome.translated : null
1100
+ });
1101
+ break;
1102
+ }
1103
+ case "reasoning":
1104
+ flushImages();
1105
+ entries.push({ type: "reasoning", key: entries.length, blockIndex: i, text: block.text ?? "" });
1106
+ break;
1107
+ case "image":
1108
+ if (imageRun === null) imageRun = [];
1109
+ imageRun.push(block.attachment);
1110
+ break;
1111
+ case "tool-call":
1112
+ flushImages();
1113
+ break;
1114
+ default:
1115
+ flushImages();
1116
+ entries.push({ type: "unknown", key: entries.length, block: block.block ?? null });
1117
+ }
1118
+ }
1119
+ flushImages();
1120
+ const showStopped = interrupted && (groupPart === void 0 || groupPart === "response" || !blocks.some((block) => block.kind !== "reasoning" && block.kind !== "tool-call"));
1121
+ if (showStopped) entries.push({ type: "stopped", key: entries.length });
1122
+ return { entries, texts };
1123
+ }
1124
+
1125
+ // src/client/chat/click-guard.ts
1126
+ function isBareBlockClick(event, view) {
1127
+ const target = event.target;
1128
+ const interactive = typeof target?.closest === "function" ? target.closest('a,button,[role="button"]') : null;
1129
+ if (interactive !== null && interactive !== void 0 && interactive !== event.currentTarget) return false;
1130
+ const selection = typeof view?.getSelection === "function" ? view.getSelection() : null;
1131
+ if (selection !== null && selection !== void 0) {
1132
+ if (selection.isCollapsed === false && String(selection.toString?.() ?? "") !== "") return false;
1133
+ }
1134
+ return true;
1135
+ }
1136
+
1137
+ // src/client/chat/styles.ts
1138
+ var STYLE_TAG_ID = "@lynn123411/dsh-chat-translate/chat-assistant.css";
1139
+ var ASSISTANT_CSS = [
1140
+ // ---- 行骨架(宿主 AssistantMarkdown.root/body 等价) ----
1141
+ ".dsh-ct-root{display:flex;flex-direction:column;font-size:var(--dsh-content-font-size,14px);line-height:calc(24px + var(--dsh-content-font-delta,0px));color:var(--dsw-alias-label-primary)}",
1142
+ ".dsh-ct-body{display:flex;flex-direction:column;gap:16px}",
1143
+ ".dsh-ct-body>[data-turn-process-inline][hidden]{margin-bottom:-16px}",
1144
+ // 宽表格溢出内容列(宿主等价规则,类名 md-table-wide 由 MarkdownText 自己产出)
1145
+ ".dsh-ct-body .md-table-wide{--dsh-table-spare:max(0px, calc((100cqw - var(--dsh-chat-content-width)) / 2));--dsh-table-lead:calc(var(--dsh-table-spare) + min(var(--dsh-chat-content-width), 100cqw) - 100%);box-sizing:border-box;width:calc(100% + var(--dsh-table-lead) + var(--dsh-table-spare));max-width:none;margin-left:calc(-1 * var(--dsh-table-lead));padding-left:var(--dsh-table-lead)}",
1146
+ ".dsh-ct-body .md-table-wide>table{z-index:1;position:relative}",
1147
+ // ---- 已停止标记(宿主 .hWmORq_stopped 等价) ----
1148
+ ".dsh-ct-stopped{border-radius:var(--dsw-radius-sm);background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-tertiary);align-self:flex-start;padding:0 6px;font-size:11px;line-height:18px}",
1149
+ // ---- 「已翻译」交互与标记:可点态常驻(含左缘内缩);区分走色相+粗细
1150
+ // (0.5px 虚线会被抗锯齿糊成实线)——蓝粗线=读译文,灰细线=读原文备着译文 ----
1151
+ ".dsh-ct-prose-clickable{padding-left:12px;cursor:pointer}",
1152
+ ".dsh-ct-prose-clickable:focus-visible{outline:1.5px solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary));outline-offset:2px;border-radius:var(--dsw-radius-sm)}",
1153
+ ".dsh-ct-prose-translated{border-left:1px solid color-mix(in srgb, var(--dsw-alias-state-business-primary) 65%, transparent)}",
1154
+ ".dsh-ct-prose-original{border-left:0.5px solid var(--dsw-alias-border-l2)}",
1155
+ // ---- ReasoningRow(宿主 lcKema_* 等价,前缀换成 dsh-ct-think) ----
1156
+ ".dsh-ct-think-root{flex-direction:column;display:flex}",
1157
+ ".dsh-ct-think-root:not([data-expanded]){contain:size layout;height:calc(24px + var(--dsh-content-font-delta,0px))}",
1158
+ ".dsh-ct-think-row{position:relative;overflow:hidden}",
1159
+ ".dsh-ct-think-root[data-expanded] [data-open] [data-disclosure-row]{z-index:1;background:var(--dsw-alias-bg-base);position:sticky;top:0}",
1160
+ '.dsh-ct-think-root[data-state=running] .dsh-ct-think-row:after{content:"";inset-block:0;background:linear-gradient(90deg, transparent 0%, color-mix(in srgb, var(--dsw-alias-bg-base) 60%, transparent) 55%, transparent 100%);pointer-events:none;width:300px;animation:2.6s ease-out infinite dsh-ct-reasoning-sweep;position:absolute;left:0}',
1161
+ "@keyframes dsh-ct-reasoning-sweep{0%{left:-300px}90%,to{left:100%}}",
1162
+ "@media (prefers-reduced-motion:reduce){.dsh-ct-think-root[data-state=running] .dsh-ct-think-row:after{animation:none}}",
1163
+ ".dsh-ct-think-leading{flex-shrink:0}",
1164
+ ".dsh-ct-think-chevron{color:var(--dsw-alias-label-secondary)}",
1165
+ ".dsh-ct-think-title{font-weight:400}",
1166
+ ".dsh-ct-think-separator{background:var(--dsw-alias-label-caption);border-radius:1px;flex:none;width:2px;height:2px;margin:0 8px}",
1167
+ ".dsh-ct-think-summary{min-width:0;color:var(--dsw-alias-label-tertiary);font-size:var(--dsh-content-font-size-secondary,13px);line-height:calc(20px + var(--dsh-content-font-delta-secondary,0px));white-space:nowrap;flex:auto;overflow:hidden}",
1168
+ ".dsh-ct-think-summary-text{text-overflow:ellipsis;display:block;overflow:hidden}",
1169
+ ".dsh-ct-think-summary[data-streaming]{mask-image:linear-gradient(90deg,currentcolor calc(100% - 48px),transparent)}",
1170
+ ".dsh-ct-think-summary[data-streaming] .dsh-ct-think-summary-text{text-overflow:clip;overflow:visible}",
1171
+ ".dsh-ct-think-root:not([data-preview]) .dsh-ct-think-separator,.dsh-ct-think-root:not([data-preview]) .dsh-ct-think-summary{display:none}",
1172
+ ".dsh-ct-think-body{padding:4px 0 4px calc(22px + var(--dsh-content-font-delta,0px));min-width:0}",
1173
+ // ---- 无障碍:屏幕阅读器专用文本 ----
1174
+ ".dsh-ct-visually-hidden{clip:rect(0 0 0 0);white-space:nowrap;width:1px;height:1px;position:absolute;overflow:hidden}"
1175
+ ].join("");
1176
+ function proseClassNames(processed, showOriginal) {
1177
+ if (!processed) return void 0;
1178
+ return showOriginal ? "dsh-ct-prose-clickable dsh-ct-prose-original" : "dsh-ct-prose-clickable dsh-ct-prose-translated";
1179
+ }
1180
+ function ensureAssistantStyles() {
1181
+ if (typeof document === "undefined") return;
1182
+ const selector = `style[data-plugin-css=${JSON.stringify(STYLE_TAG_ID)}]`;
1183
+ if (document.querySelector(selector) !== null) return;
1184
+ const tag = document.createElement("style");
1185
+ tag.dataset.plugin = "@lynn123411/dsh-chat-translate";
1186
+ tag.dataset.pluginCss = STYLE_TAG_ID;
1187
+ tag.textContent = ASSISTANT_CSS;
1188
+ document.head.appendChild(tag);
1189
+ }
1190
+
1191
+ // src/client/chat/assistant-step.tsx
1192
+ var thinkIcon = () => React.createElement(import_dsh_client_ui_primitives3.IconThinkOutlineRegular, { size: 14 });
1193
+ function markdownLabels(t) {
1194
+ return {
1195
+ code: {
1196
+ copyLabel: t("copy"),
1197
+ copiedLabel: t("copied"),
1198
+ toolbarLabels: {
1199
+ codeLabel: t("codeBlock.title"),
1200
+ wrapLabel: t("codeBlock.wrap"),
1201
+ unwrapLabel: t("codeBlock.unwrap")
1202
+ }
1203
+ },
1204
+ footnotes: t("markdown.footnotes")
1205
+ };
1206
+ }
1207
+ function isWindowsStylePath(value) {
1208
+ return /^[A-Za-z]:[/\\]/.test(value) || value.startsWith("\\\\");
1209
+ }
1210
+ function isAbsoluteWorkspacePath(path) {
1211
+ return path.startsWith("/") || isWindowsStylePath(path);
1212
+ }
1213
+ function fileMediaUrl(base, path) {
1214
+ if (!/^https?:/u.test(base) && !base.startsWith("dsh-app://app/") || !isAbsoluteWorkspacePath(path)) return void 0;
1215
+ if (/^[/\\]{2}/u.test(path) || /[\u0000-\u001f\u007f]/u.test(path)) return void 0;
1216
+ return new URL(`api/file?path=${encodeURIComponent(path)}`, base).href;
1217
+ }
1218
+ function localPathMediaUrl(base, value) {
1219
+ let path;
1220
+ try {
1221
+ path = decodeURIComponent(value.split(/[?#]/u)[0] ?? "");
1222
+ } catch {
1223
+ return void 0;
1224
+ }
1225
+ return fileMediaUrl(base, path);
1226
+ }
1227
+ function firstLine(text) {
1228
+ const newline = text.indexOf("\n");
1229
+ return newline === -1 ? text : text.slice(0, newline);
1230
+ }
1231
+ function latestCompletedParagraphFirstLine(text) {
1232
+ let summary = "";
1233
+ let paragraphStart = 0;
1234
+ const separator = /\r?\n(?:[\t ]*\r?\n)+/g;
1235
+ for (; ; ) {
1236
+ const nextParagraph = separator.exec(text);
1237
+ const paragraphEnd = nextParagraph === null ? text.length : nextParagraph.index + nextParagraph[0].indexOf("\n");
1238
+ const newline = text.indexOf("\n", paragraphStart);
1239
+ if (newline !== -1 && newline <= paragraphEnd) {
1240
+ const candidate = text.slice(paragraphStart, newline).trim();
1241
+ if (candidate !== "") summary = candidate;
1242
+ }
1243
+ if (nextParagraph === null) return summary;
1244
+ paragraphStart = nextParagraph.index + nextParagraph[0].length;
1245
+ }
1246
+ }
1247
+ function useSearchableHidden(hidden, reveal) {
1248
+ const ref = React.useRef(null);
1249
+ React.useLayoutEffect(() => {
1250
+ const element = ref.current;
1251
+ if (element === null) return;
1252
+ if (hidden && element.contains(element.ownerDocument.activeElement)) {
1253
+ reveal();
1254
+ return;
1255
+ }
1256
+ if (hidden) element.setAttribute("hidden", "until-found");
1257
+ else element.removeAttribute("hidden");
1258
+ }, [hidden, reveal]);
1259
+ React.useEffect(() => {
1260
+ const element = ref.current;
1261
+ if (element === null) return;
1262
+ element.addEventListener("beforematch", reveal);
1263
+ return () => {
1264
+ element.removeEventListener("beforematch", reveal);
1265
+ };
1266
+ }, [reveal]);
1267
+ return ref;
1268
+ }
1269
+ var subscribeSettings = (listener) => settingsStore.subscribe(listener);
1270
+ var subscribeTranslate = (listener) => chatTranslate.subscribe(listener);
1271
+ function useSettledInView(ref, armed) {
1272
+ const noObserver = typeof IntersectionObserver === "undefined";
1273
+ const [inView, setInView] = (0, import_react4.useState)(noObserver);
1274
+ (0, import_react4.useEffect)(() => {
1275
+ if (noObserver || !armed) return;
1276
+ const element = ref.current;
1277
+ if (element === null) return;
1278
+ const observer = new IntersectionObserver(
1279
+ (entries) => {
1280
+ setInView(entries.some((entry) => entry.isIntersecting));
1281
+ },
1282
+ { rootMargin: "150px" }
1283
+ );
1284
+ observer.observe(element);
1285
+ return () => {
1286
+ observer.disconnect();
1287
+ };
1288
+ }, [armed, noObserver, ref]);
1289
+ return inView;
1290
+ }
1291
+ var ReasoningRow = (0, import_react4.memo)(function ReasoningRow2({
1292
+ text,
1293
+ running,
1294
+ usePresentation,
1295
+ useDisclosure,
1296
+ t
1297
+ }) {
1298
+ const { expanded, toggle } = useDisclosure();
1299
+ const labels = (0, import_react4.useMemo)(() => markdownLabels(t), [t]);
1300
+ const summaryText = running ? latestCompletedParagraphFirstLine(text) : firstLine(text);
1301
+ const summary = (0, import_react4.useMemo)(() => summaryText.replaceAll("**", ""), [summaryText]);
1302
+ const preview = usePresentation(
1303
+ (policy2) => !expanded && summary !== "" && (running || policy2.settledReasoningPreview)
1304
+ );
1305
+ const collapsedContent = (0, import_react4.useMemo)(
1306
+ () => React.createElement(
1307
+ React.Fragment,
1308
+ null,
1309
+ React.createElement("span", { className: "dsh-ct-think-separator", "aria-hidden": true }),
1310
+ React.createElement(
1311
+ "span",
1312
+ { className: "dsh-ct-think-summary", "data-streaming": running || void 0 },
1313
+ React.createElement("span", { className: "dsh-ct-think-summary-text" }, summary)
1314
+ )
1315
+ ),
1316
+ [running, summary]
1317
+ );
1318
+ const content = (0, import_react4.useMemo)(
1319
+ () => expanded ? React.createElement(
1320
+ "div",
1321
+ { className: "dsh-ct-think-body" },
1322
+ React.createElement(import_dsh_client_ui_primitives3.MarkdownText, { text, streaming: running, labels, variant: "compact" })
1323
+ ) : void 0,
1324
+ [expanded, labels, running, text]
1325
+ );
1326
+ return React.createElement(
1327
+ "div",
1328
+ {
1329
+ className: "dsh-ct-think-root",
1330
+ "data-variant": "think",
1331
+ "data-state": running ? "running" : "ok",
1332
+ "data-expanded": expanded || void 0,
1333
+ "data-preview": preview || void 0
1334
+ },
1335
+ running && React.createElement("span", { className: "dsh-ct-visually-hidden" }, t("row.running")),
1336
+ React.createElement(
1337
+ import_dsh_client_ui_primitives3.DisclosureRow,
1338
+ {
1339
+ rowClassName: "dsh-ct-think-row",
1340
+ leadingClassName: "dsh-ct-think-leading",
1341
+ titleClassName: "dsh-ct-think-title",
1342
+ chevronClassName: "dsh-ct-think-chevron",
1343
+ icon: thinkIcon(),
1344
+ title: t("message.think"),
1345
+ open: expanded,
1346
+ expandable: true,
1347
+ expandOnRowClick: true,
1348
+ onToggle: toggle,
1349
+ collapsedContent
1350
+ },
1351
+ content
1352
+ )
1353
+ );
1354
+ });
1355
+ function blockClick(event, onToggle) {
1356
+ const guardEvent = {
1357
+ target: event.target,
1358
+ currentTarget: event.currentTarget
1359
+ };
1360
+ if (isBareBlockClick(guardEvent, typeof window === "undefined" ? void 0 : window)) onToggle();
1361
+ }
1362
+ function ProseBlock({
1363
+ text,
1364
+ translated,
1365
+ showOriginal,
1366
+ onToggle,
1367
+ streaming,
1368
+ labels,
1369
+ mentions,
1370
+ pathImages
1371
+ }) {
1372
+ const processed = translated !== null;
1373
+ const showTranslation = processed && !showOriginal;
1374
+ const source = showTranslation ? translated : text;
1375
+ return React.createElement(
1376
+ "div",
1377
+ {
1378
+ // 色相+粗细随显示态:蓝粗线=译文,灰细线=有译文但读原文(仍可点切回),无线=没译。
1379
+ className: proseClassNames(processed, showOriginal),
1380
+ "data-translated": showTranslation ? "true" : void 0,
1381
+ // 容器内含链接等交互内容,不套 role=button(非法嵌套);可聚焦 + 键盘
1382
+ // Enter/Space 即切换,满足官方「键盘可达」门。
1383
+ tabIndex: processed ? 0 : void 0,
1384
+ onClick: processed ? (event) => blockClick(event, onToggle) : void 0,
1385
+ onKeyDown: processed ? (event) => {
1386
+ if (event.target !== event.currentTarget) return;
1387
+ if (event.key === "Enter" || event.key === " ") {
1388
+ event.preventDefault();
1389
+ onToggle();
1390
+ }
1391
+ } : void 0
1392
+ },
1393
+ React.createElement(import_dsh_client_ui_primitives3.MarkdownText, {
1394
+ text: source,
1395
+ streaming,
1396
+ labels,
1397
+ fileMentions: mentions,
1398
+ pathImages
1399
+ })
1400
+ );
1401
+ }
1402
+ var AssistantStepView = (0, import_react4.memo)(function AssistantStepView2(props) {
1403
+ ensureAssistantStyles();
1404
+ const { node, groupPart, useDisclosure, useTurnData, turnProcess, openFile, renderMessageImages, fileMentions, usePresentation, t } = props;
1405
+ const data = node.data;
1406
+ const streaming = data.status === "running";
1407
+ const interrupted = data.status === "interrupted";
1408
+ const enabled = (0, import_react4.useSyncExternalStore)(subscribeSettings, () => settingsStore.getState().enabled);
1409
+ const aiConfigured = (0, import_react4.useSyncExternalStore)(subscribeSettings, () => settingsStore.getState().aiConfigured);
1410
+ (0, import_react4.useSyncExternalStore)(subscribeTranslate, () => chatTranslate.getVersion());
1411
+ const canTranslate = enabled && aiConfigured;
1412
+ const rowKey = `a${node.anchorSeq ?? `t${data.turn}:s${data.step}`}`;
1413
+ const labels = (0, import_react4.useMemo)(() => markdownLabels(t), [t]);
1414
+ const pathImages = (0, import_react4.useMemo)(
1415
+ () => ({ resolve: (value) => localPathMediaUrl(document.baseURI, value) }),
1416
+ []
1417
+ );
1418
+ const turn = node.location?.kind === "turn" || node.location?.kind === "step" ? node.location.turn : void 0;
1419
+ const tail = useTurnData("turn-tail");
1420
+ const owner = (0, import_react4.useMemo)(() => {
1421
+ if (turn?.status !== "closed" || data.finalNode === void 0) return void 0;
1422
+ if (tail?.closing?.finalNode?.seq !== data.finalNode.seq) return void 0;
1423
+ return { turn, seq: data.finalNode.seq, openFile };
1424
+ }, [data.finalNode, openFile, tail, turn]);
1425
+ const mentions = (0, import_react4.useMemo)(() => owner === void 0 ? void 0 : fileMentions(owner), [fileMentions, owner]);
1426
+ const blocks = (0, import_react4.useMemo)(() => data.blocks ?? [], [data.blocks]);
1427
+ const plan = (0, import_react4.useMemo)(
1428
+ () => planAssistantRow({
1429
+ blocks,
1430
+ streaming,
1431
+ interrupted,
1432
+ groupPart,
1433
+ canTranslate,
1434
+ outcomes: void 0
1435
+ }),
1436
+ [blocks, streaming, interrupted, groupPart, canTranslate]
1437
+ );
1438
+ const rootRef = (0, import_react4.useRef)(null);
1439
+ const inView = useSettledInView(rootRef, !streaming && canTranslate && plan.entries !== null && plan.texts.length > 0);
1440
+ (0, import_react4.useEffect)(() => {
1441
+ if (streaming || !canTranslate || !inView || plan.entries === null || plan.texts.length === 0) return;
1442
+ chatTranslate.ensure(rowKey, plan.texts);
1443
+ }, [streaming, canTranslate, inView, rowKey, plan]);
1444
+ const rowState = chatTranslate.getState(rowKey);
1445
+ const displayed = (0, import_react4.useMemo)(
1446
+ () => planAssistantRow({
1447
+ blocks,
1448
+ streaming,
1449
+ interrupted,
1450
+ groupPart,
1451
+ canTranslate,
1452
+ outcomes: rowState?.outcomes
1453
+ }),
1454
+ [blocks, streaming, interrupted, groupPart, canTranslate, rowState]
1455
+ );
1456
+ const reasoningHidden = turnProcess !== void 0 && turnProcess.foldable && turnProcess.spec.answerStep === data.step && turnProcess.spec.inlineReasoning && !turnProcess.open;
1457
+ const revealProcess = (0, import_react4.useCallback)(() => {
1458
+ turnProcess?.setOpen(true);
1459
+ }, [turnProcess]);
1460
+ const [originalKeys, setOriginalKeys] = (0, import_react4.useState)(() => /* @__PURE__ */ new Set());
1461
+ const toggleBlock = (0, import_react4.useCallback)((key) => {
1462
+ setOriginalKeys((current) => {
1463
+ const next = new Set(current);
1464
+ if (next.has(key)) next.delete(key);
1465
+ else next.add(key);
1466
+ return next;
1467
+ });
1468
+ }, []);
1469
+ if (displayed.entries === null) return null;
1470
+ const last = blocks.length - 1;
1471
+ const rendered = [];
1472
+ for (const entry of displayed.entries) {
1473
+ switch (entry.type) {
1474
+ case "prose":
1475
+ rendered.push(
1476
+ React.createElement(ProseBlock, {
1477
+ key: entry.key,
1478
+ text: entry.text,
1479
+ translated: entry.translated,
1480
+ showOriginal: originalKeys.has(entry.key),
1481
+ onToggle: () => toggleBlock(entry.key),
1482
+ streaming,
1483
+ labels,
1484
+ mentions,
1485
+ pathImages
1486
+ })
1487
+ );
1488
+ break;
1489
+ case "reasoning":
1490
+ rendered.push(
1491
+ React.createElement(
1492
+ ProcessReasoning,
1493
+ {
1494
+ key: entry.key,
1495
+ hidden: reasoningHidden,
1496
+ reveal: revealProcess,
1497
+ children: React.createElement(ReasoningRow, {
1498
+ text: entry.text,
1499
+ running: streaming && entry.blockIndex === last,
1500
+ usePresentation,
1501
+ useDisclosure,
1502
+ t
1503
+ })
1504
+ }
1505
+ )
1506
+ );
1507
+ break;
1508
+ case "images":
1509
+ rendered.push(
1510
+ React.createElement(React.Fragment, {
1511
+ key: entry.key
1512
+ }, renderMessageImages({ images: entry.attachments.map((attachment) => ({ attachment })), align: "start" }))
1513
+ );
1514
+ break;
1515
+ case "unknown":
1516
+ rendered.push(
1517
+ React.createElement(import_dsh_client_ui_primitives3.JsonBlock, {
1518
+ key: entry.key,
1519
+ label: t("message.unknownBlock"),
1520
+ payload: entry.block,
1521
+ truncatedLabel: (total) => t("json.truncated", { total })
1522
+ })
1523
+ );
1524
+ break;
1525
+ case "stopped":
1526
+ rendered.push(React.createElement("span", { key: entry.key, className: "dsh-ct-stopped" }, t("message.stopped")));
1527
+ break;
1528
+ }
1529
+ }
1530
+ return React.createElement(
1531
+ "div",
1532
+ { ref: rootRef, className: "dsh-ct-root", "data-streaming": streaming || void 0 },
1533
+ React.createElement("div", { className: "dsh-ct-body" }, rendered)
1534
+ );
1535
+ });
1536
+ function ProcessReasoning(props) {
1537
+ const ref = useSearchableHidden(props.hidden, props.reveal);
1538
+ return React.createElement(
1539
+ "div",
1540
+ { ref, "data-turn-process-inline": props.hidden || void 0 },
1541
+ props.children
1542
+ );
1543
+ }
1544
+
1545
+ // src/client/chat/presentation.ts
1546
+ function policy(mode, foldCompletedTurns, stepGrouping, liveProcessDetail, settledReasoningPreview) {
1547
+ return Object.freeze({ mode, foldCompletedTurns, stepGrouping, liveProcessDetail, settledReasoningPreview });
1548
+ }
1549
+ var POLICY_BY_MODE = Object.freeze({
1550
+ compact: policy("compact", true, "collapsed", false, false),
1551
+ standard: policy("standard", true, "collapsed", true, true),
1552
+ detailed: policy("detailed", true, "history", true, true),
1553
+ verbose: policy("verbose", false, "none", false, true),
1554
+ normal: policy("standard", true, "collapsed", true, true),
1555
+ expanded: policy("detailed", true, "history", true, true)
1556
+ });
1557
+ var DEFAULT_MODE = "standard";
1558
+ var ChatPresentation = class {
1559
+ policy = POLICY_BY_MODE[DEFAULT_MODE];
1560
+ listeners = /* @__PURE__ */ new Set();
1561
+ form = null;
1562
+ unsubscribe = null;
1563
+ attach(form) {
1564
+ this.detach();
1565
+ if (!form) {
1566
+ this.policy = POLICY_BY_MODE[DEFAULT_MODE];
1567
+ return;
1568
+ }
1569
+ this.form = form;
1570
+ this.read();
1571
+ this.unsubscribe = form.subscribe(() => this.read());
1572
+ }
1573
+ detach() {
1574
+ this.unsubscribe?.();
1575
+ this.unsubscribe = null;
1576
+ this.form = null;
1577
+ }
1578
+ getSnapshot = () => this.policy;
1579
+ subscribe = (listener) => {
1580
+ this.listeners.add(listener);
1581
+ return () => {
1582
+ this.listeners.delete(listener);
1583
+ };
1584
+ };
1585
+ read() {
1586
+ const form = this.form;
1587
+ if (!form) return;
1588
+ let value;
1589
+ try {
1590
+ const snap = form.getSnapshot();
1591
+ value = snap.status === "ready" ? snap.value : void 0;
1592
+ } catch {
1593
+ value = void 0;
1594
+ }
1595
+ const raw = typeof value?.transcriptView === "string" ? value.transcriptView : "";
1596
+ const next = POLICY_BY_MODE[raw] ?? POLICY_BY_MODE[DEFAULT_MODE];
1597
+ if (next !== this.policy) {
1598
+ this.policy = next;
1599
+ for (const listener of [...this.listeners]) {
1600
+ try {
1601
+ listener();
1602
+ } catch {
1603
+ }
1604
+ }
1605
+ }
1606
+ }
1607
+ };
1608
+ function createChatPresentation() {
1609
+ return new ChatPresentation();
1610
+ }
1611
+ var chatPresentation = createChatPresentation();
1612
+
1669
1613
  // src/client/index.ts
1670
1614
  var name = "dsh-chat-translate";
1671
1615
  var inject = ["slots", "configForms", "remote", "remote.credentials", "locale"];
1672
1616
  function apply(ctx) {
1673
- ctx.effect(() => chatTranslateObserver.start(document), "dsh-chat-translate: reply translate observer");
1617
+ ensureAssistantStyles();
1618
+ ctx.effect(() => {
1619
+ const slots = ctx.slots;
1620
+ if (!slots || typeof slots.inject !== "function") return () => {
1621
+ };
1622
+ return slots.inject(
1623
+ "conversation.chat.node",
1624
+ () => slots.register(
1625
+ {
1626
+ name: "conversation.chat.node",
1627
+ key: "assistant-step",
1628
+ // 行内的折叠、停止、未知块等 chrome 文案复用宿主 `chat` 词典,
1629
+ // 两侧渲染逐字一致;t 由座次按该命名空间合成。
1630
+ locale: "chat",
1631
+ priority: -1,
1632
+ // 思考行的 settledReasoningPreview 来自活的工作细节模式。
1633
+ inject: () => ({ hooks: { presentation: chatPresentation } })
1634
+ },
1635
+ AssistantStepView
1636
+ )
1637
+ );
1638
+ }, "dsh-chat-translate: assistant step takeover");
1639
+ ctx.effect(() => {
1640
+ let form = null;
1641
+ try {
1642
+ form = ctx.configForms?.get("ui-chat") ?? null;
1643
+ } catch {
1644
+ form = null;
1645
+ }
1646
+ chatPresentation.attach(form);
1647
+ return () => chatPresentation.detach();
1648
+ }, "dsh-chat-translate: chat presentation source");
1674
1649
  ctx.effect(() => setupSettingsUi(ctx), "dsh-chat-translate: settings section");
1650
+ ctx.effect(() => setupComposerToggle(ctx), "dsh-chat-translate: composer dock toggle");
1675
1651
  }
1676
1652
  return module.exports; } });
1677
1653
  //# sourceMappingURL=client.js.map