@lynn123411/dsh-chat-translate 1.9.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -22,13 +22,16 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
22
22
  var index_exports = {};
23
23
  __export(index_exports, {
24
24
  NonDestructiveTranslationMount: () => NonDestructiveTranslationMount,
25
- StreamDebounceViewportObserver: () => StreamDebounceViewportObserver,
26
25
  apply: () => apply,
27
26
  chatTranslateObserver: () => chatTranslateObserver,
27
+ chunkUnits: () => chunkUnits,
28
28
  clientCache: () => clientCache,
29
+ collectReplyUnits: () => collectReplyUnits,
29
30
  inject: () => inject,
30
31
  lazyQueue: () => lazyQueue,
31
32
  name: () => name,
33
+ needsTranslation: () => needsTranslation,
34
+ replyTranslator: () => replyTranslator,
32
35
  settingsStore: () => settingsStore,
33
36
  setupSettingsUi: () => setupSettingsUi
34
37
  });
@@ -55,10 +58,9 @@ function isMaskLeak(translatedText) {
55
58
  }
56
59
 
57
60
  // src/client/translate/client-cache.ts
58
- var TITLE_CACHE_KEY = "dsh-chat-translate:cache";
59
- var TITLE_MAX_ENTRIES = 500;
60
- var THINK_CACHE_KEY = "dsh-chat-translate:think-cache";
61
- var THINK_MAX_ENTRIES = 300;
61
+ var CACHE_KEY = "dsh-chat-translate:cache";
62
+ var MAX_ENTRIES = 500;
63
+ var RETIRED_CACHE_KEYS = ["dsh-chat-translate:think-cache"];
62
64
  var TTL_MS = 7 * 24 * 60 * 60 * 1e3;
63
65
  var ClientCache = class {
64
66
  // Map preserves insertion order in JS, enabling true LRU semantics.
@@ -67,7 +69,7 @@ var ClientCache = class {
67
69
  saveTimer = null;
68
70
  storageKey;
69
71
  maxEntries;
70
- constructor(storageKey = TITLE_CACHE_KEY, maxEntries = TITLE_MAX_ENTRIES) {
72
+ constructor(storageKey = CACHE_KEY, maxEntries = MAX_ENTRIES) {
71
73
  this.storageKey = storageKey;
72
74
  this.maxEntries = maxEntries;
73
75
  this.load();
@@ -159,8 +161,17 @@ var ClientCache = class {
159
161
  return this.memCache.size;
160
162
  }
161
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();
162
174
  var clientCache = new ClientCache();
163
- var thinkClientCache = new ClientCache(THINK_CACHE_KEY, THINK_MAX_ENTRIES);
164
175
 
165
176
  // src/describe-error.ts
166
177
  function describeError(err) {
@@ -189,51 +200,7 @@ function describeError(err) {
189
200
  }
190
201
 
191
202
  // src/client/translate/api.ts
192
- async function requestTranslateBatch(texts, options = {}) {
193
- if (!Array.isArray(texts) || texts.length === 0) return [];
194
- const validTexts = texts.map((t) => typeof t === "string" ? t : "");
195
- if (validTexts.length === 0) return [];
196
- const controller = new AbortController();
197
- const timeoutId = options.timeoutMs ? setTimeout(() => controller.abort(), options.timeoutMs) : null;
198
- const effectiveSignal = options.signal ? options.signal.aborted ? options.signal : controller.signal : controller.signal;
199
- if (options.signal) {
200
- options.signal.addEventListener("abort", () => controller.abort(), { once: true });
201
- }
202
- try {
203
- const res = await fetch("/api/dsh-chat-translate/translate", {
204
- method: "POST",
205
- headers: {
206
- "Content-Type": "application/json"
207
- },
208
- body: JSON.stringify({ texts: validTexts }),
209
- signal: effectiveSignal
210
- });
211
- if (res.ok) {
212
- const data = await res.json();
213
- if (data.ok && Array.isArray(data.results) && data.results.length > 0) {
214
- return data.results;
215
- } else if (data.error) {
216
- console.warn(`[dsh-chat-translate] \u7FFB\u8BD1\u63A5\u53E3\u9519\u8BEF: ${data.error}`);
217
- }
218
- } else {
219
- console.warn(`[dsh-chat-translate] \u7FFB\u8BD1\u8BF7\u6C42\u5931\u8D25 (HTTP ${res.status})`);
220
- }
221
- } catch (err) {
222
- if (err?.name === "AbortError") {
223
- }
224
- } finally {
225
- if (timeoutId !== null) {
226
- clearTimeout(timeoutId);
227
- }
228
- }
229
- return validTexts.map((t) => ({
230
- original: t,
231
- translated: t,
232
- channel: "fallback-client",
233
- cached: false
234
- }));
235
- }
236
- async function requestTranslateThink(blocks) {
203
+ async function requestTranslateReply(blocks) {
237
204
  const fallback = () => blocks.map((block) => ({
238
205
  original: block,
239
206
  translated: block,
@@ -243,7 +210,7 @@ async function requestTranslateThink(blocks) {
243
210
  }));
244
211
  if (!Array.isArray(blocks) || blocks.length === 0) return [];
245
212
  try {
246
- const res = await fetch("/api/dsh-chat-translate/translate-think", {
213
+ const res = await fetch("/api/dsh-chat-translate/translate-reply", {
247
214
  method: "POST",
248
215
  headers: { "Content-Type": "application/json" },
249
216
  body: JSON.stringify({ blocks })
@@ -254,13 +221,13 @@ async function requestTranslateThink(blocks) {
254
221
  return data.results;
255
222
  }
256
223
  if (data.error) {
257
- console.warn(`[dsh-chat-translate] \u601D\u8003\u94FE\u7FFB\u8BD1\u63A5\u53E3\u9519\u8BEF: ${data.error}`);
224
+ console.warn(`[dsh-chat-translate] \u6B63\u6587\u7FFB\u8BD1\u63A5\u53E3\u9519\u8BEF: ${data.error}`);
258
225
  }
259
226
  } else {
260
- console.warn(`[dsh-chat-translate] \u601D\u8003\u94FE\u7FFB\u8BD1\u8BF7\u6C42\u5931\u8D25 (HTTP ${res.status})`);
227
+ console.warn(`[dsh-chat-translate] \u6B63\u6587\u7FFB\u8BD1\u8BF7\u6C42\u5931\u8D25 (HTTP ${res.status})`);
261
228
  }
262
229
  } catch (err) {
263
- console.warn(`[dsh-chat-translate] \u601D\u8003\u94FE\u7FFB\u8BD1\u8BF7\u6C42\u5F02\u5E38: ${describeError(err)}`);
230
+ console.warn(`[dsh-chat-translate] \u6B63\u6587\u7FFB\u8BD1\u8BF7\u6C42\u5F02\u5E38: ${describeError(err)}`);
264
231
  }
265
232
  return fallback();
266
233
  }
@@ -377,29 +344,6 @@ var NonDestructiveTranslationMount = class {
377
344
  static isOwnNode(element) {
378
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;
379
346
  }
380
- /**
381
- * 把一个作用域里的全部挂载译文一起翻面。思考链只由卡片标题右侧的按钮调用
382
- * 它,正文里的点击不再改变原文与译文。
383
- */
384
- static toggleGroup(scope, showOriginal) {
385
- const mounted = scope.querySelectorAll('[data-tidy-translated="true"]');
386
- for (const element of mounted) {
387
- const origWrapper = element.querySelector(
388
- `:scope > .${CLASS_ORIGINAL_HIDDEN}, :scope > .${CLASS_ORIGINAL_SHOWN}`
389
- );
390
- const transWrapper = element.querySelector(`:scope > .${CLASS_TRANSLATED_BLOCK}`);
391
- if (!origWrapper || !transWrapper) continue;
392
- if (showOriginal) {
393
- origWrapper.style.display = "inline";
394
- origWrapper.className = CLASS_ORIGINAL_SHOWN;
395
- transWrapper.style.display = "none";
396
- } else {
397
- origWrapper.style.display = "none";
398
- origWrapper.className = CLASS_ORIGINAL_HIDDEN;
399
- transWrapper.style.display = "inline";
400
- }
401
- }
402
- }
403
347
  /**
404
348
  * Unmounts translation and restores original DOM nodes completely.
405
349
  */
@@ -458,215 +402,7 @@ var NonDestructiveTranslationMount = class {
458
402
  }
459
403
  };
460
404
 
461
- // src/client/translate/viewport-observer.ts
462
- var StreamDebounceViewportObserver = class {
463
- intersectionObserver = null;
464
- /**
465
- * Pending per-element streaming debounce timers. A Map (not a WeakMap)
466
- * because disconnect() must enumerate and clear every pending timer: a
467
- * WeakMap cannot be iterated, so a debounce armed just before the switch was
468
- * turned off would still fire register() afterwards.
469
- */
470
- streamingTimers = /* @__PURE__ */ new Map();
471
- pendingQueue = [];
472
- batchFlushTimer = null;
473
- options;
474
- constructor(options) {
475
- this.options = {
476
- rootMargin: options.rootMargin ?? "150px 0px",
477
- debounceMs: options.debounceMs ?? 400,
478
- onVisibleBatch: options.onVisibleBatch
479
- };
480
- this.initIntersectionObserver();
481
- }
482
- initIntersectionObserver() {
483
- if (typeof window === "undefined" || typeof IntersectionObserver === "undefined") {
484
- return;
485
- }
486
- this.intersectionObserver = new IntersectionObserver(
487
- (entries) => {
488
- for (const entry of entries) {
489
- if (entry.isIntersecting && entry.target instanceof HTMLElement) {
490
- const el = entry.target;
491
- this.intersectionObserver?.unobserve(el);
492
- const text = el.dataset.tidyPendingText || NonDestructiveTranslationMount.extractVisibleText(el) || "";
493
- if (text) {
494
- delete el.dataset.tidyPendingText;
495
- this.enqueueBatch(el, text);
496
- }
497
- }
498
- }
499
- },
500
- {
501
- root: null,
502
- // viewport
503
- rootMargin: this.options.rootMargin,
504
- threshold: 0
505
- }
506
- );
507
- }
508
- /**
509
- * Observe an element with streaming debounce.
510
- * If streaming updates characterData repeatedly within debounceMs, the timer resets.
511
- */
512
- observeWithDebounce(element, text, immediate = false) {
513
- this.armDebounce(element, text, immediate);
514
- }
515
- armDebounce(element, text, immediate) {
516
- if (!element || !text) return;
517
- const existingTimer = this.streamingTimers.get(element);
518
- if (existingTimer !== void 0) {
519
- clearTimeout(existingTimer);
520
- this.streamingTimers.delete(element);
521
- }
522
- if (immediate || this.options.debounceMs <= 0) {
523
- this.register(element, text);
524
- return;
525
- }
526
- const timer = window.setTimeout(() => {
527
- if (this.streamingTimers.get(element) !== timer) return;
528
- this.streamingTimers.delete(element);
529
- if (element.isConnected) {
530
- const latestText = NonDestructiveTranslationMount.extractVisibleText(element) || text;
531
- this.register(element, latestText);
532
- }
533
- }, this.options.debounceMs);
534
- this.streamingTimers.set(element, timer);
535
- }
536
- register(element, text) {
537
- if (!element.isConnected) return;
538
- if (!this.intersectionObserver) {
539
- this.enqueueBatch(element, text);
540
- return;
541
- }
542
- element.dataset.tidyPendingText = text;
543
- this.intersectionObserver.observe(element);
544
- }
545
- enqueueBatch(element, text) {
546
- this.pendingQueue.push({ element, text });
547
- if (this.batchFlushTimer === null && typeof window !== "undefined") {
548
- this.batchFlushTimer = window.setTimeout(() => {
549
- this.batchFlushTimer = null;
550
- this.flushQueue();
551
- }, 50);
552
- }
553
- }
554
- flushQueue() {
555
- if (this.pendingQueue.length === 0) return;
556
- const batch = [...this.pendingQueue];
557
- this.pendingQueue = [];
558
- this.options.onVisibleBatch(batch);
559
- }
560
- unobserve(element) {
561
- const timer = this.streamingTimers.get(element);
562
- if (timer !== void 0) {
563
- clearTimeout(timer);
564
- this.streamingTimers.delete(element);
565
- }
566
- delete element.dataset.tidyPendingText;
567
- if (this.intersectionObserver) {
568
- this.intersectionObserver.unobserve(element);
569
- }
570
- }
571
- disconnect() {
572
- if (this.batchFlushTimer !== null) {
573
- clearTimeout(this.batchFlushTimer);
574
- this.batchFlushTimer = null;
575
- }
576
- for (const timer of this.streamingTimers.values()) {
577
- clearTimeout(timer);
578
- }
579
- this.streamingTimers.clear();
580
- this.pendingQueue = [];
581
- if (this.intersectionObserver) {
582
- this.intersectionObserver.disconnect();
583
- this.initIntersectionObserver();
584
- }
585
- }
586
- };
587
-
588
- // src/client/translate/lazy.ts
589
- var LazyTranslationQueue = class {
590
- enabled = true;
591
- viewportObserver;
592
- constructor() {
593
- this.viewportObserver = new StreamDebounceViewportObserver({
594
- rootMargin: "150px 0px",
595
- debounceMs: 400,
596
- onVisibleBatch: (items) => this.handleVisibleBatch(items)
597
- });
598
- }
599
- setEnabled(enabled) {
600
- this.enabled = enabled;
601
- if (!enabled) {
602
- this.viewportObserver.disconnect();
603
- }
604
- }
605
- observe(element, text, immediate = false) {
606
- if (!this.enabled || !element.isConnected) return;
607
- const cached = clientCache.get(text);
608
- if (cached) {
609
- this.applyTranslation(element, cached, text);
610
- return;
611
- }
612
- this.viewportObserver.observeWithDebounce(element, text, immediate);
613
- }
614
- async handleVisibleBatch(items) {
615
- if (!this.enabled || items.length === 0) return;
616
- const sorted = [...items].sort((a, b) => {
617
- if (a.element === b.element) return 0;
618
- const pos = a.element.compareDocumentPosition(b.element);
619
- return pos & Node.DOCUMENT_POSITION_FOLLOWING ? -1 : 1;
620
- });
621
- const textMap = /* @__PURE__ */ new Map();
622
- for (const item of sorted) {
623
- if (!item.element.isConnected) continue;
624
- const cached = clientCache.get(item.text);
625
- if (cached) {
626
- if (this.stillMatches(item.element, item.text)) {
627
- this.applyTranslation(item.element, cached, item.text);
628
- }
629
- continue;
630
- }
631
- const list = textMap.get(item.text) || [];
632
- list.push(item.element);
633
- textMap.set(item.text, list);
634
- }
635
- const uniqueTexts = Array.from(textMap.keys());
636
- if (uniqueTexts.length === 0) return;
637
- const results = await requestTranslateBatch(uniqueTexts);
638
- for (const res of results) {
639
- const entries = textMap.get(res.original) || [];
640
- if (res.translated && res.translated.trim() && res.channel !== "fallback" && res.channel !== "fallback-client" && res.translated.trim() !== res.original.trim()) {
641
- clientCache.set(res.original, res.translated);
642
- for (const entry of entries) {
643
- if (!entry.isConnected || !this.enabled) continue;
644
- if (!this.stillMatches(entry, res.original)) continue;
645
- this.applyTranslation(entry, res.translated, res.original);
646
- }
647
- } else {
648
- console.debug(`[dsh-chat-translate] \u7FFB\u8BD1\u672A\u6210\u529F (\u964D\u7EA7\u4FDD\u7559\u539F\u6587): "${res.original.slice(0, 40)}"`);
649
- }
650
- }
651
- }
652
- /** 元素当前承载的原文是否仍是这次结果对应的那一份。 */
653
- stillMatches(element, original) {
654
- const current = NonDestructiveTranslationMount.extractVisibleText(element);
655
- return !current || current === original;
656
- }
657
- applyTranslation(element, translated, original) {
658
- if (!element.isConnected || !this.enabled) return;
659
- NonDestructiveTranslationMount.mount(element, translated, {
660
- originalText: original
661
- });
662
- }
663
- disconnect() {
664
- this.viewportObserver.disconnect();
665
- }
666
- };
667
- var lazyQueue = new LazyTranslationQueue();
668
-
669
- // src/server/pipeline/think.ts
405
+ // src/server/pipeline/blocks.ts
670
406
  var CJK_CHAR = /[\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff\u3000-\u303f\uff00-\uffef]/;
671
407
  function estimateTokens(text) {
672
408
  let cjk = 0;
@@ -676,118 +412,7 @@ function estimateTokens(text) {
676
412
  return cjk + Math.ceil((text.length - cjk) / 3);
677
413
  }
678
414
 
679
- // src/client/translate/think-button.ts
680
- var THINK_BUTTON_CLASS = "dsh-tidy-think-button";
681
- var STYLE_TAG_ID = "@lynn123411/dsh-chat-translate/think-button.css";
682
- var THINK_BUTTON_CSS = `
683
- /* \u601D\u8003\u5361\u6807\u9898\u53F3\u4FA7\u7684\u7FFB\u8BD1\u6309\u94AE\uFF1A\u4E09\u6001\uFF08\u672A\u7FFB\u8BD1 / \u7FFB\u8BD1\u4E2D / \u5DF2\u7FFB\u8BD1\uFF09\u4E0E\u65CB\u8F6C\u52A8\u753B\u3002 */
684
- .dsh-tidy-think-button {
685
- display: inline-flex;
686
- align-items: center;
687
- justify-content: center;
688
- position: relative;
689
- flex: none;
690
- width: 18px;
691
- height: 18px;
692
- margin: 0 6px 0 2px;
693
- padding: 0;
694
- border: 1px solid var(--dsw-alias-border-l2, rgba(128, 128, 128, 0.35));
695
- border-radius: 6px;
696
- background: transparent;
697
- color: var(--dsw-alias-label-tertiary, currentColor);
698
- font-family: inherit;
699
- font-size: 10px;
700
- line-height: 1;
701
- cursor: pointer;
702
- }
703
-
704
- .dsh-tidy-think-button:hover:not([disabled]) {
705
- border-color: var(--dsw-alias-label-secondary, rgba(128, 128, 128, 0.6));
706
- color: var(--dsw-alias-label-primary, currentColor);
707
- }
708
-
709
- .dsh-tidy-think-button[data-state='translated'] {
710
- border-color: transparent;
711
- background: color-mix(in srgb, var(--dsw-static-deepseek-500, #4d6bfe) 18%, transparent);
712
- color: var(--dsw-static-deepseek-500, #4d6bfe);
713
- }
714
-
715
- .dsh-tidy-think-button[data-state='working'] {
716
- color: transparent;
717
- }
718
-
719
- .dsh-tidy-think-button[data-state='working']::after {
720
- content: '';
721
- position: absolute;
722
- inset: 3px;
723
- border: 1.5px solid var(--dsw-alias-label-caption, rgba(128, 128, 128, 0.45));
724
- border-top-color: var(--dsw-static-deepseek-500, #4d6bfe);
725
- border-radius: 50%;
726
- animation: dsh-tidy-think-spin 0.8s linear infinite;
727
- }
728
-
729
- .dsh-tidy-think-button[disabled] {
730
- opacity: 0.35;
731
- cursor: default;
732
- }
733
-
734
- @keyframes dsh-tidy-think-spin {
735
- to { transform: rotate(360deg); }
736
- }
737
-
738
- @media (prefers-reduced-motion: reduce) {
739
- .dsh-tidy-think-button[data-state='working']::after {
740
- animation: none;
741
- border-color: var(--dsw-static-deepseek-500, #4d6bfe);
742
- }
743
- }
744
- `;
745
- function ensureThinkButtonStyles() {
746
- if (typeof document === "undefined") return;
747
- if (document.querySelector('style[data-plugin-css="' + STYLE_TAG_ID + '"]') !== null) return;
748
- const style = document.createElement("style");
749
- style.dataset.plugin = "@lynn123411/dsh-chat-translate";
750
- style.dataset.pluginCss = STYLE_TAG_ID;
751
- style.textContent = THINK_BUTTON_CSS;
752
- document.head?.appendChild(style);
753
- }
754
- function createThinkButton(onActivate) {
755
- ensureThinkButtonStyles();
756
- const element = document.createElement("button");
757
- element.type = "button";
758
- element.className = THINK_BUTTON_CLASS;
759
- element.textContent = "\u8BD1";
760
- element.dataset.state = "idle";
761
- element.setAttribute("aria-label", "\u7FFB\u8BD1\u601D\u8003\u94FE");
762
- element.title = "\u7FFB\u8BD1\u601D\u8003\u94FE";
763
- element.addEventListener("click", (event) => {
764
- event.stopPropagation();
765
- event.preventDefault();
766
- if (element.disabled) return;
767
- onActivate();
768
- });
769
- const setState = (state) => {
770
- element.dataset.state = state;
771
- const label = state === "translated" ? "\u663E\u793A\u601D\u8003\u539F\u6587" : "\u7FFB\u8BD1\u601D\u8003\u94FE";
772
- element.setAttribute("aria-label", label);
773
- element.title = label;
774
- };
775
- return {
776
- element,
777
- getState: () => element.dataset.state ?? "idle",
778
- setState,
779
- setDisabled: (disabled, reason) => {
780
- element.disabled = disabled;
781
- if (disabled) element.title = reason;
782
- else setState(element.dataset.state ?? "idle");
783
- }
784
- };
785
- }
786
-
787
- // src/client/translate/think.ts
788
- var THINK_CARD_SELECTOR = '[data-variant="think"]';
789
- var THINK_BODY_SELECTOR = '[class*="thinkBody" i]';
790
- var EXPANDED_SELECTOR = "[data-open]";
415
+ // src/client/translate/reply.ts
791
416
  var BLOCK_TAGS = /* @__PURE__ */ new Set([
792
417
  "P",
793
418
  "H1",
@@ -807,19 +432,22 @@ var BLOCK_TAGS = /* @__PURE__ */ new Set([
807
432
  var CONTAINER_TAGS = /* @__PURE__ */ new Set(["UL", "OL", "DL", "TABLE", "THEAD", "TBODY", "TR", "DIV", "SECTION"]);
808
433
  var NESTED_BOUNDARY_SELECTOR = [...BLOCK_TAGS, ...CONTAINER_TAGS, "PRE"].join(",").toLowerCase();
809
434
  var REQUEST_TOKENS = 2048;
810
- var SYNC_DEBOUNCE_MS = 120;
435
+ var HAS_LATIN = /[A-Za-z]/;
811
436
  function isMountedElement(element) {
812
437
  return element.dataset?.tidyTranslated === "true";
813
438
  }
814
439
  function hasNestedBoundary(element) {
815
440
  return element.querySelector(NESTED_BOUNDARY_SELECTOR) !== null;
816
441
  }
817
- function collectThinkUnits(body) {
442
+ function needsTranslation(text) {
443
+ return HAS_LATIN.test(text);
444
+ }
445
+ function collectReplyUnits(body) {
818
446
  const units = [];
819
447
  const pushRun = (nodes) => {
820
448
  if (nodes.length === 0) return;
821
449
  const text = nodes.map((node) => node.textContent ?? "").join("").trim();
822
- if (!text) return;
450
+ if (!text || !needsTranslation(text)) return;
823
451
  const anchor = nodes[0];
824
452
  if (anchor === void 0) return;
825
453
  units.push({ nodes, text, anchor });
@@ -862,7 +490,7 @@ function collectThinkUnits(body) {
862
490
  if (isMountedElement(element)) return;
863
491
  if (!hasNestedBoundary(element)) {
864
492
  const text = (element.textContent ?? "").trim();
865
- if (text) units.push({ element, text, anchor: element });
493
+ if (text && needsTranslation(text)) units.push({ element, text, anchor: element });
866
494
  return;
867
495
  }
868
496
  walk(element);
@@ -887,226 +515,65 @@ function chunkUnits(units, maxTokens = REQUEST_TOKENS) {
887
515
  if (current.length > 0) chunks.push(current);
888
516
  return chunks;
889
517
  }
890
- var ThinkTranslateController = class {
891
- /** 总开关与设置里的思考链开关同时打开。 */
892
- enabled = false;
893
- /** AI 通道开关打开且已配置;缺任何一条都不注入按钮。 */
894
- configured = false;
895
- states = /* @__PURE__ */ new WeakMap();
896
- syncTimers = /* @__PURE__ */ new WeakMap();
897
- /** 只处理当前会话的容器;由观察器在会话切换时更新。 */
898
- scope = null;
899
- /** 观察器把当前会话的根交给控制器,插入按钮时只看这一棵子树。 */
900
- setScope(scope) {
901
- this.scope = scope;
902
- }
903
- isEnabled() {
904
- return this.enabled;
905
- }
906
- setEnabled(enabled) {
907
- if (enabled === this.enabled) return;
908
- this.enabled = enabled;
909
- this.applyVisibility();
910
- }
911
- setConfigured(configured) {
912
- if (configured === this.configured) return;
913
- this.configured = configured;
914
- this.applyVisibility();
915
- }
916
- /** 按钮是否可以存在:两个开关与 AI 通道配置缺一不可。 */
917
- usable() {
918
- return this.enabled && this.configured;
919
- }
920
- applyVisibility() {
921
- if (typeof document === "undefined") return;
922
- if (this.usable()) {
923
- this.syncAll();
924
- return;
925
- }
926
- for (const element of document.querySelectorAll("." + THINK_BUTTON_CLASS)) {
927
- element.remove();
928
- }
929
- for (const card of document.querySelectorAll(THINK_CARD_SELECTOR)) {
930
- const state = this.states.get(card);
931
- if (state !== void 0) {
932
- state.button = null;
933
- state.state = "idle";
934
- state.expandedHandled = false;
935
- }
936
- NonDestructiveTranslationMount.restore(card);
937
- }
938
- }
939
- /** 扫描当前会话里的全部思考卡(开关、配置或会话变化后调用)。 */
940
- syncAll() {
941
- const scope = this.scope ?? (typeof document === "undefined" ? null : document);
942
- if (scope === null) return;
943
- for (const card of scope.querySelectorAll(THINK_CARD_SELECTOR)) {
944
- this.syncCard(card);
945
- }
946
- }
947
- /** 防抖后同步一张卡片。 */
948
- syncCard(card) {
949
- if (typeof window === "undefined") return;
950
- const existing = this.syncTimers.get(card);
951
- if (existing !== void 0) window.clearTimeout(existing);
952
- const timer = window.setTimeout(() => {
953
- this.syncTimers.delete(card);
954
- this.runSync(card);
955
- }, SYNC_DEBOUNCE_MS);
956
- this.syncTimers.set(card, timer);
957
- }
958
- stateFor(card) {
959
- let state = this.states.get(card);
960
- if (state === void 0) {
961
- state = { button: null, state: "idle", expandedHandled: false };
962
- this.states.set(card, state);
963
- }
964
- return state;
965
- }
966
- runSync(card) {
967
- if (!card.isConnected) return;
968
- const state = this.stateFor(card);
969
- if (!this.usable()) {
970
- this.discardCard(card, state);
971
- return;
972
- }
973
- const expanded = card.querySelector(EXPANDED_SELECTOR) !== null;
974
- if (expanded) this.ensureButton(card, state);
975
- else {
976
- this.removeButton(state);
977
- state.expandedHandled = false;
978
- }
979
- if (state.state !== "working") {
980
- state.state = this.isShowingTranslation(card) ? "translated" : "idle";
981
- }
982
- state.button?.setState(state.state);
983
- this.updateDisabled(card, state);
984
- if (!expanded || state.expandedHandled || state.state === "working") return;
985
- state.expandedHandled = true;
986
- this.showCachedWhenComplete(card, state);
987
- }
988
- updateDisabled(card, state) {
989
- const running = card.dataset.state === "running";
990
- state.button?.setDisabled(running && state.state !== "working", "\u601D\u8003\u8F93\u51FA\u4E2D\uFF0C\u7ED3\u675F\u540E\u53EF\u7FFB\u8BD1");
991
- }
992
- ensureButton(card, state) {
993
- if (state.button !== null) return;
994
- const row = card.querySelector("[data-disclosure-row]");
995
- if (row === null) return;
996
- const button = createThinkButton(() => this.activate(card));
997
- const title = row.querySelector('[class*="title" i]');
998
- if (title !== null && title.parentElement === row) {
999
- row.insertBefore(button.element, title.nextSibling);
1000
- } else {
1001
- row.appendChild(button.element);
1002
- }
1003
- state.button = button;
1004
- button.setState(state.state);
1005
- }
1006
- /** 折叠或停用时撤掉按钮,卡片的翻译状态不变。 */
1007
- removeButton(state) {
1008
- state.button?.element.remove();
1009
- state.button = null;
1010
- }
1011
- /** 插件停用:撤掉按钮并还原这张卡片上的全部译文。 */
1012
- discardCard(card, state) {
1013
- this.removeButton(state);
1014
- state.state = "idle";
1015
- state.expandedHandled = false;
1016
- NonDestructiveTranslationMount.restore(card);
1017
- }
1018
- /** 卡片当前显示的是译文(至少有一个译文块可见)。 */
1019
- isShowingTranslation(card) {
1020
- const blocks = card.querySelectorAll("." + CLASS_TRANSLATED_BLOCK);
1021
- for (const block of blocks) {
1022
- if (block.style.display !== "none") return true;
1023
- }
1024
- return false;
1025
- }
1026
- bodyOf(card) {
1027
- return card.querySelector(THINK_BODY_SELECTOR);
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;
1028
535
  }
1029
- /** 展开时若整条都命中缓存,直接显示中文,不点按钮也不发请求。 */
1030
- showCachedWhenComplete(card, state) {
1031
- const body = this.bodyOf(card);
1032
- if (body === null || state.state === "working") return;
1033
- const units = collectThinkUnits(body);
536
+ async run(body) {
537
+ const units = collectReplyUnits(body);
1034
538
  if (units.length === 0) return;
1035
- const cached = units.map((unit) => thinkClientCache.get(unit.text));
1036
- if (cached.some((value) => value === void 0)) return;
1037
- units.forEach((unit, index) => this.applyUnit(unit, cached[index]));
1038
- state.state = this.isShowingTranslation(card) ? "translated" : "idle";
1039
- state.button?.setState(state.state);
1040
- }
1041
- /** 按钮被点击;按钮只在展开的卡片上存在。 */
1042
- activate(card) {
1043
- const state = this.stateFor(card);
1044
- if (!this.usable() || state.state === "working") return;
1045
- if (card.dataset.state === "running") return;
1046
- if (state.state === "translated") {
1047
- NonDestructiveTranslationMount.toggleGroup(card, true);
1048
- state.state = "idle";
1049
- state.button?.setState("idle");
1050
- return;
1051
- }
1052
- this.translateCard(card, state);
1053
- }
1054
- /** 翻译这张卡片的全部单位:命中缓存的先挂上,其余按块请求。 */
1055
- translateCard(card, state) {
1056
- const body = this.bodyOf(card);
1057
- if (body === null) return;
1058
- if (card.querySelector("." + CLASS_TRANSLATED_BLOCK) !== null) {
1059
- NonDestructiveTranslationMount.toggleGroup(card, false);
1060
- }
1061
- const units = collectThinkUnits(body);
1062
- if (units.length === 0) {
1063
- state.state = this.isShowingTranslation(card) ? "translated" : "idle";
1064
- state.button?.setState(state.state);
1065
- return;
1066
- }
1067
539
  const missing = [];
1068
540
  for (const unit of units) {
1069
- const cached = thinkClientCache.get(unit.text);
541
+ const cached = clientCache.get(unit.text);
1070
542
  if (cached === void 0) missing.push(unit);
1071
543
  else this.applyUnit(unit, cached);
1072
544
  }
1073
- if (missing.length === 0) {
1074
- state.state = this.isShowingTranslation(card) ? "translated" : "idle";
1075
- state.button?.setState(state.state);
1076
- return;
1077
- }
1078
- state.state = "working";
1079
- state.button?.setState("working");
1080
- void this.translateUnits(card, state, missing);
1081
- }
1082
- async translateUnits(card, state, units) {
1083
- let failed = false;
1084
- for (const chunk of chunkUnits(units, REQUEST_TOKENS)) {
1085
- if (!this.usable()) {
1086
- failed = true;
1087
- break;
1088
- }
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)) {
1089
554
  const texts = chunk.map((unit) => unit.text);
1090
- let results = [];
555
+ let results;
1091
556
  try {
1092
- results = await requestTranslateThink(texts);
1093
- } catch {
1094
- failed = true;
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;
1095
563
  }
1096
564
  const byText = new Map(results.map((result) => [result.original, result]));
1097
565
  for (const unit of chunk) {
1098
566
  const result = byText.get(unit.text);
1099
567
  if (result === void 0 || !result.ok || !result.translated.trim() || result.translated.trim() === unit.text.trim()) {
1100
- failed = true;
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
+ );
1101
571
  continue;
1102
572
  }
1103
- thinkClientCache.set(unit.text, result.translated);
573
+ clientCache.set(unit.text, result.translated);
1104
574
  this.applyUnit(unit, result.translated);
1105
575
  }
1106
576
  }
1107
- state.state = !failed && this.isShowingTranslation(card) ? "translated" : "idle";
1108
- state.button?.setState(state.state);
1109
- this.updateDisabled(card, state);
1110
577
  }
1111
578
  /** 把译文挂到单位上;期间文本或节点已经变动的单位直接放弃(结果已进缓存)。 */
1112
579
  applyUnit(unit, translated) {
@@ -1115,8 +582,7 @@ var ThinkTranslateController = class {
1115
582
  if (!element.isConnected || isMountedElement(element)) return;
1116
583
  if ((element.textContent ?? "").trim() !== unit.text) return;
1117
584
  NonDestructiveTranslationMount.mount(element, translated, {
1118
- originalText: unit.text,
1119
- interactive: false
585
+ originalText: unit.text
1120
586
  });
1121
587
  return;
1122
588
  }
@@ -1127,58 +593,131 @@ var ThinkTranslateController = class {
1127
593
  const text = nodes.map((node) => node.textContent ?? "").join("").trim();
1128
594
  if (text !== unit.text) return;
1129
595
  NonDestructiveTranslationMount.mountRun(nodes, translated, {
1130
- originalText: unit.text,
1131
- interactive: false
596
+ originalText: unit.text
1132
597
  });
1133
598
  }
1134
599
  };
1135
- var thinkTranslator = new ThinkTranslateController();
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();
1136
674
 
1137
675
  // src/client/translate/observer.ts
1138
- var TOOL_TITLE_SELECTOR = '[class*="summary" i]';
1139
- var THINK_CARD_SELECTOR2 = '[data-variant="think"]';
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(",");
1140
686
  var SESSION_ROOT_SELECTOR = "[data-conversation-scroll], [data-chat-flow]";
1141
687
  var ROOT_CHECK_INTERVAL_MS = 3e3;
1142
- function isToolSummarySpan(span) {
1143
- if (!span || span.nodeType !== 1) return false;
1144
- if (span.hasAttribute("aria-hidden")) return false;
1145
- if (span.closest(THINK_CARD_SELECTOR2)) return false;
1146
- if (span.querySelector?.('[class*="title"], [class*="leading"], [class*="chevron"], [class*="sep"], [class*="summary" i]')) {
1147
- return false;
1148
- }
1149
- const cls = span.className || "";
1150
- if (/title|leading|icon|badge|chevron|separator|sep\b|row\b|root\b|card\b/i.test(cls)) return false;
1151
- const rawToggle = (span.textContent || "").trim();
1152
- if (rawToggle.length <= 12 && /^(展开|收起|展开全部|收起全部|Expand|Collapse|Show more|Show less|Think|思考)$/i.test(rawToggle)) {
1153
- return false;
1154
- }
1155
- if (rawToggle === "Think" || rawToggle === "\u601D\u8003") return false;
1156
- if (span.closest('button, [role="button"]') && rawToggle.length <= 12 && /展开|收起|Expand|Collapse|Think|思考/i.test(rawToggle)) {
1157
- return false;
1158
- }
1159
- if (span.closest(
1160
- '[data-chat-call-id], [data-slot="tool.call.toolview"], [data-sample], [data-variant], [data-tool], [data-disclosure-row]'
1161
- )) {
1162
- return true;
1163
- }
1164
- return false;
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;
1165
697
  }
1166
698
  var ChatTranslateObserver = class {
1167
699
  observer = null;
1168
700
  rootElement = null;
1169
701
  rootCheckTimer = null;
1170
702
  isEnabled = true;
1171
- /** 设置里的思考链开关;与总开关一起决定按钮是否存在。 */
1172
- thinkEnabled = false;
703
+ /**
704
+ * 已经交给翻译器的正文容器,避免同一条回答在每次 DOM 变动时重扫。总开关重开
705
+ * 时清空:关着的那段时间落定的回答需要补翻。
706
+ */
707
+ handled = /* @__PURE__ */ new WeakSet();
1173
708
  constructor() {
1174
709
  this.handleMutations = this.handleMutations.bind(this);
1175
710
  }
1176
711
  setEnabled(enabled) {
712
+ if (enabled === this.isEnabled) return;
1177
713
  this.isEnabled = enabled;
1178
- thinkTranslator.setEnabled(enabled && this.thinkEnabled);
1179
714
  if (enabled) {
1180
715
  lazyQueue.setEnabled(true);
716
+ this.handled = /* @__PURE__ */ new WeakSet();
717
+ this.observer?.disconnect();
718
+ this.observer = null;
1181
719
  this.start();
720
+ if (this.rootElement !== null) this.scanContainer(this.rootElement);
1182
721
  } else {
1183
722
  this.restoreOriginals();
1184
723
  this.disconnect();
@@ -1188,21 +727,7 @@ var ChatTranslateObserver = class {
1188
727
  restoreOriginals() {
1189
728
  const scope = this.rootElement ?? document;
1190
729
  NonDestructiveTranslationMount.restore(scope);
1191
- }
1192
- /**
1193
- * 设置里的思考链开关:关掉就撤掉所有翻译按钮并还原思考卡里的译文,工具标题
1194
- * 译文不动。
1195
- */
1196
- setThinkEnabled(enabled) {
1197
- this.thinkEnabled = enabled;
1198
- thinkTranslator.setEnabled(this.isEnabled && enabled);
1199
- }
1200
- /**
1201
- * AI 通道的可用性(通道开关打开且 Key / Base URL / 模型齐全)。不可用时同样
1202
- * 不注入按钮:思考链翻译只能走这条通道。
1203
- */
1204
- setThinkConfigured(configured) {
1205
- thinkTranslator.setConfigured(configured);
730
+ this.handled = /* @__PURE__ */ new WeakSet();
1206
731
  }
1207
732
  /**
1208
733
  * The active session's scroll container. Only the currently-viewed session
@@ -1238,9 +763,7 @@ var ChatTranslateObserver = class {
1238
763
  const findAndObserveRoot = () => {
1239
764
  const root = this.findRoot(documentRef);
1240
765
  this.rootElement = root;
1241
- thinkTranslator.setScope(root);
1242
766
  this.scanContainer(root);
1243
- this.scanThink(root);
1244
767
  if (!this.observer) {
1245
768
  this.observer = new MutationObserver(this.handleMutations);
1246
769
  this.observer.observe(root, {
@@ -1248,14 +771,14 @@ var ChatTranslateObserver = class {
1248
771
  subtree: true,
1249
772
  attributes: true,
1250
773
  characterData: true,
774
+ // `data-streaming` 从 markdown 根上消失就是落定信号;`data-turn-process-*`
775
+ // 是内核改判「哪些 flow item 属于折叠块」时动的属性,它一变就要重扫。
1251
776
  attributeFilter: [
1252
- "data-state",
1253
- "data-tool",
1254
- "data-variant",
1255
- "data-sample",
1256
- "aria-expanded",
1257
- "data-open",
1258
- "data-expanded"
777
+ "data-streaming",
778
+ "data-chat-flow-kind",
779
+ "data-chat-group-part",
780
+ "data-turn-process-member",
781
+ "data-turn-process-answer"
1259
782
  ]
1260
783
  });
1261
784
  }
@@ -1288,131 +811,101 @@ var ChatTranslateObserver = class {
1288
811
  for (const mutation of mutations) {
1289
812
  if (mutation.type === "childList") {
1290
813
  let addedElement = false;
1291
- let changedOwner = null;
1292
814
  for (let i = 0; i < mutation.addedNodes.length; i++) {
1293
815
  const node = mutation.addedNodes[i];
1294
816
  if (node instanceof HTMLElement) {
1295
- const owner = this.mountedOwner(node);
1296
- if (owner) {
1297
- changedOwner = owner;
1298
- continue;
1299
- }
1300
817
  if (NonDestructiveTranslationMount.isOwnNode(node)) continue;
1301
818
  addedElement = true;
1302
819
  this.scanNode(node);
1303
- this.scanThink(node);
1304
820
  }
1305
821
  }
1306
822
  const target = mutation.target;
1307
823
  if (target instanceof HTMLElement) {
1308
- const owner = this.mountedOwner(target);
1309
- if (owner) {
1310
- changedOwner = owner;
1311
- } else if (!addedElement && !NonDestructiveTranslationMount.isOwnNode(target)) {
824
+ if (!addedElement && !NonDestructiveTranslationMount.isOwnNode(target)) {
1312
825
  this.scanNode(target);
1313
- this.scanThink(target);
1314
826
  }
1315
827
  }
1316
- if (changedOwner) this.handleOriginalTextChange(changedOwner);
1317
828
  } else if (mutation.type === "attributes") {
1318
829
  const target = mutation.target;
1319
830
  if (target instanceof HTMLElement) {
1320
831
  if (NonDestructiveTranslationMount.isOwnNode(target)) continue;
1321
832
  this.scanNode(target);
1322
- this.scanThink(target);
1323
833
  }
1324
834
  } else if (mutation.type === "characterData") {
1325
- const owner = this.mountedOwner(mutation.target);
1326
- if (owner) {
1327
- this.handleOriginalTextChange(owner);
1328
- continue;
1329
- }
1330
835
  const parent = mutation.target.parentElement;
1331
836
  if (parent instanceof HTMLElement) {
1332
837
  if (NonDestructiveTranslationMount.isOwnNode(parent)) continue;
1333
838
  this.scanNode(parent);
1334
- this.scanThink(parent);
1335
839
  }
1336
840
  }
1337
841
  }
1338
842
  }
1339
843
  /**
1340
- * 变更发生在我们移进隐藏容器的原文里时,返回挂载了译文的那个元素。React
1341
- * 就地改写它自己创建的文本节点,所以「原文变了」只能从这条路径看到。
844
+ * 扫一棵子树,把里面**全部**该翻的正文容器交给翻译器。
845
+ *
846
+ * 不再「命中第一个就返回」:一个容器里可以有好几行 response(折叠块里每一步
847
+ * 露出的一段正文就是一行),只取第一个会把后面那些整片漏掉。
1342
848
  */
1343
- mountedOwner(node) {
1344
- const element = node instanceof HTMLElement ? node : node.parentElement;
1345
- if (!element) return null;
1346
- const wrapper = element.closest(
1347
- `.${CLASS_ORIGINAL_HIDDEN}, .${CLASS_ORIGINAL_SHOWN}`
1348
- );
1349
- if (!wrapper) return null;
1350
- const owner = wrapper.parentElement;
1351
- return owner instanceof HTMLElement && owner.dataset.tidyTranslated === "true" ? owner : null;
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
+ }
1352
855
  }
1353
- /**
1354
- * 已挂载行的隐藏原文被上游改写了(ask_user 计数、失败摘要、todo 计数):
1355
- * 命中缓存就原地换译文,否则请求新译文后原地换。行始终可见,不出现英文。
1356
- */
1357
- handleOriginalTextChange(owner) {
1358
- if (!isToolSummarySpan(owner)) return;
1359
- const current = NonDestructiveTranslationMount.extractVisibleText(owner);
1360
- if (!current || current === owner.dataset.original) return;
1361
- const cached = clientCache.get(current);
1362
- if (cached) {
1363
- NonDestructiveTranslationMount.mount(owner, cached, { originalText: current });
1364
- return;
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);
1365
863
  }
1366
- lazyQueue.observe(owner, current);
1367
864
  }
1368
- scanContainer(container) {
1369
- const spans = container.querySelectorAll(TOOL_TITLE_SELECTOR);
1370
- spans.forEach((span) => {
1371
- if (isToolSummarySpan(span)) {
1372
- this.processSpan(span);
1373
- }
1374
- });
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
+ }
1375
872
  }
1376
- scanNode(node) {
1377
- if (node.matches?.(TOOL_TITLE_SELECTOR) && isToolSummarySpan(node)) {
1378
- this.processSpan(node);
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);
1379
879
  }
1380
- const spans = node.querySelectorAll(TOOL_TITLE_SELECTOR);
1381
- spans.forEach((span) => {
1382
- if (isToolSummarySpan(span)) {
1383
- this.processSpan(span);
1384
- }
1385
- });
880
+ }
881
+ /** 判定并交给翻译器:这一个元素自己是不是一条该翻的正文容器。 */
882
+ tryBody(element) {
883
+ if (!(element instanceof HTMLElement)) return;
884
+ if (!this.isTranslatableBody(element)) return;
885
+ this.requestTranslate(element);
1386
886
  }
1387
887
  /**
1388
- * 交给思考正文翻译控制器:命中节点所在的卡片,以及它下面(或它就是)的
1389
- * 全部思考卡片。
888
+ * 这个元素自己是不是一条该翻的正文容器:落定的、落在过程折叠块里的 markdown
889
+ * 正文,且不被排除规则挡住(Think 卡、工具行、最终汇总都不算)。
1390
890
  */
1391
- scanThink(node) {
1392
- if (!thinkTranslator.isEnabled()) return;
1393
- if (node instanceof HTMLElement) {
1394
- const card = node.closest(THINK_CARD_SELECTOR2);
1395
- if (card) thinkTranslator.syncCard(card);
1396
- }
1397
- if (!node.querySelectorAll) return;
1398
- node.querySelectorAll(THINK_CARD_SELECTOR2).forEach((card) => {
1399
- thinkTranslator.syncCard(card);
1400
- });
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;
1401
900
  }
1402
- processSpan(span) {
1403
- if (NonDestructiveTranslationMount.isMounted(span)) {
1404
- return;
1405
- }
1406
- const text = NonDestructiveTranslationMount.extractVisibleText(span);
1407
- if (!text) return;
1408
- const cached = clientCache.get(text);
1409
- if (cached) {
1410
- NonDestructiveTranslationMount.mount(span, cached, {
1411
- originalText: text
1412
- });
1413
- return;
1414
- }
1415
- lazyQueue.observe(span, text);
901
+ /**
902
+ * 把一条落定的正文交给视口懒队列:视口内立刻翻、视口外的等滚到再翻,
903
+ * 命中缓存的由翻译器立即挂上。同一条回答只登记一次。
904
+ */
905
+ requestTranslate(body) {
906
+ if (this.handled.has(body)) return;
907
+ this.handled.add(body);
908
+ lazyQueue.observe(body);
1416
909
  }
1417
910
  disconnect() {
1418
911
  if (this.rootCheckTimer !== null) {
@@ -1434,17 +927,13 @@ var import_react2 = require("react");
1434
927
  var import_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
1435
928
 
1436
929
  // src/client/settings/store.ts
1437
- var THINK_TIMEOUT_MIN = 500;
1438
- var THINK_TIMEOUT_MAX = 9e5;
930
+ var AI_TIMEOUT_MIN = 500;
931
+ var AI_TIMEOUT_MAX = 9e5;
1439
932
  var SETTINGS_NAMESPACE = "dsh-chat-translate";
1440
933
  var TRANSLATE_API_KEY_REF = "TRANSLATE_API_KEY";
1441
934
  var DEFAULT_STATE = {
1442
935
  enabled: true,
1443
- concurrency: 3,
1444
- aiEnabled: true,
1445
- bingEnabled: true,
1446
- thinkEnabled: true,
1447
- thinkTimeoutMs: 6e5,
936
+ aiTimeoutMs: 6e5,
1448
937
  baseUrl: "",
1449
938
  model: "",
1450
939
  aiConfigured: false
@@ -1492,18 +981,11 @@ var SettingsStore = class {
1492
981
  if (!value || typeof value !== "object") return;
1493
982
  const next = { ...this.state };
1494
983
  if (typeof value.enabled === "boolean") next.enabled = value.enabled;
1495
- const c = value.concurrency;
1496
- if (typeof c === "number" && Number.isFinite(c)) {
1497
- next.concurrency = Math.min(Math.max(Math.round(c), 1), 100);
1498
- }
1499
- if (typeof value.aiEnabled === "boolean") next.aiEnabled = value.aiEnabled;
1500
- if (typeof value.bingEnabled === "boolean") next.bingEnabled = value.bingEnabled;
1501
- if (typeof value.thinkEnabled === "boolean") next.thinkEnabled = value.thinkEnabled;
1502
- const thinkTimeout = value.thinkTimeoutMs;
1503
- if (typeof thinkTimeout === "number" && Number.isFinite(thinkTimeout)) {
1504
- next.thinkTimeoutMs = Math.min(
1505
- Math.max(Math.round(thinkTimeout), THINK_TIMEOUT_MIN),
1506
- THINK_TIMEOUT_MAX
984
+ const timeout = value.aiTimeoutMs;
985
+ if (typeof timeout === "number" && Number.isFinite(timeout)) {
986
+ next.aiTimeoutMs = Math.min(
987
+ Math.max(Math.round(timeout), AI_TIMEOUT_MIN),
988
+ AI_TIMEOUT_MAX
1507
989
  );
1508
990
  }
1509
991
  if (typeof value.baseUrl === "string") next.baseUrl = value.baseUrl;
@@ -1520,11 +1002,6 @@ var SettingsStore = class {
1520
1002
  } catch {
1521
1003
  }
1522
1004
  }
1523
- try {
1524
- chatTranslateObserver.setThinkEnabled(this.state.enabled && this.state.thinkEnabled);
1525
- chatTranslateObserver.setThinkConfigured(this.state.aiEnabled && this.state.aiConfigured);
1526
- } catch {
1527
- }
1528
1005
  this.notify();
1529
1006
  }
1530
1007
  notify() {
@@ -1560,35 +1037,21 @@ var SettingsStore = class {
1560
1037
  * (conflict-safe recovery).
1561
1038
  */
1562
1039
  async update(partial) {
1563
- let sanitizedConcurrency = this.state.concurrency;
1564
- if (typeof partial.concurrency === "number" && Number.isFinite(partial.concurrency)) {
1565
- sanitizedConcurrency = Math.min(Math.max(Math.round(partial.concurrency), 1), 100);
1566
- }
1567
- let sanitizedThinkTimeout = this.state.thinkTimeoutMs;
1568
- if (typeof partial.thinkTimeoutMs === "number" && Number.isFinite(partial.thinkTimeoutMs)) {
1569
- sanitizedThinkTimeout = Math.min(
1570
- Math.max(Math.round(partial.thinkTimeoutMs), THINK_TIMEOUT_MIN),
1571
- THINK_TIMEOUT_MAX
1040
+ let sanitizedTimeout = this.state.aiTimeoutMs;
1041
+ if (typeof partial.aiTimeoutMs === "number" && Number.isFinite(partial.aiTimeoutMs)) {
1042
+ sanitizedTimeout = Math.min(
1043
+ Math.max(Math.round(partial.aiTimeoutMs), AI_TIMEOUT_MIN),
1044
+ AI_TIMEOUT_MAX
1572
1045
  );
1573
1046
  }
1574
1047
  const next = {
1575
1048
  ...this.state,
1576
1049
  ...partial,
1577
- concurrency: sanitizedConcurrency,
1578
- thinkTimeoutMs: sanitizedThinkTimeout
1050
+ aiTimeoutMs: sanitizedTimeout
1579
1051
  };
1580
1052
  this.applyState(next);
1581
1053
  if (this.form) {
1582
- const fields = [
1583
- "enabled",
1584
- "concurrency",
1585
- "aiEnabled",
1586
- "bingEnabled",
1587
- "thinkEnabled",
1588
- "thinkTimeoutMs",
1589
- "baseUrl",
1590
- "model"
1591
- ];
1054
+ const fields = ["enabled", "aiTimeoutMs", "baseUrl", "model"];
1592
1055
  for (const field of fields) {
1593
1056
  if (partial[field] !== void 0) {
1594
1057
  this.pendingFields.add(field);
@@ -1736,10 +1199,6 @@ var SETTINGS_CSS = String.raw`
1736
1199
  width: 120px;
1737
1200
  }
1738
1201
 
1739
- .dsh-tidy-field-count {
1740
- width: 88px;
1741
- }
1742
-
1743
1202
  .dsh-tidy-test-result {
1744
1203
  margin-left: 10px;
1745
1204
  font: var(--dsw-font-xxs-12);
@@ -1753,13 +1212,6 @@ var SETTINGS_CSS = String.raw`
1753
1212
  color: var(--dsw-alias-state-error-primary);
1754
1213
  }
1755
1214
 
1756
- .dsh-tidy-behavior-list {
1757
- display: flex;
1758
- flex-direction: column;
1759
- gap: 4px;
1760
- margin: 4px 0 0;
1761
- padding-left: 18px;
1762
- }
1763
1215
  `;
1764
1216
 
1765
1217
  // src/client/reading-settings-page.tsx
@@ -1899,91 +1351,67 @@ function claimReadingSettingsPage(ctx, label, locale) {
1899
1351
  var NS = "settings.chatTranslate";
1900
1352
  var zh = {
1901
1353
  pageNav: "\u9605\u8BFB\u4F53\u9A8C",
1902
- title: "\u804A\u5929\u7FFB\u8BD1",
1354
+ title: "\u6B63\u6587\u7FFB\u8BD1",
1903
1355
  masterTitle: "\u7FFB\u8BD1\u603B\u5F00\u5173",
1904
1356
  enableTranslation: "\u542F\u7528\u7FFB\u8BD1",
1905
- masterDesc: "\u81EA\u52A8\u5C06\u5F53\u524D\u4F1A\u8BDD\u4E2D\u5DE5\u5177\u8C03\u7528\u6807\u9898\uFF08\u5982 {example}\uFF09\u7FFB\u8BD1\u4E3A\u4E2D\u6587\uFF0C\u70B9\u51FB\u8BD1\u6587\u53EF\u539F\u5730\u5207\u6362\u539F\u6587/\u8BD1\u6587\u3002\u601D\u8003\u6B63\u6587\u9ED8\u8BA4\u4E0D\u7FFB\u8BD1\uFF0C\u7531 AI \u5361\u7247\u91CC\u7684\u300C\u663E\u793A\u601D\u8003\u94FE\u7FFB\u8BD1\u6309\u94AE\u300D\u63A7\u5236\uFF1A\u6253\u5F00\u540E\u5C55\u5F00\u7684\u601D\u8003\u5361\u53F3\u4FA7\u51FA\u73B0\u6309\u94AE\uFF0C\u70B9\u51FB\u624D\u7FFB\u8BD1\u3002\u601D\u8003\u5361\u6298\u53E0\u8D77\u6765\u7684\u90A3\u4E00\u884C\u6458\u8981\u6C38\u4E0D\u7FFB\u8BD1\u3002\u4EC5\u4F5C\u7528\u4E8E\u5F53\u524D\u67E5\u770B\u7684\u4F1A\u8BDD\uFF0C\u56DE\u7B54\u6B63\u6587\u6C38\u4E0D\u7FFB\u8BD1\u3002",
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",
1906
1358
  aiTitle: "AI \u7FFB\u8BD1\uFF08OpenAI \u517C\u5BB9\u534F\u8BAE\uFF09",
1907
- enableAi: "\u542F\u7528 AI \u7FFB\u8BD1\u901A\u9053",
1359
+ 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",
1908
1360
  badgeConfigured: "\u5DF2\u914D\u7F6E",
1909
1361
  badgeUnconfigured: "\u672A\u914D\u7F6E",
1910
- aiFallbackNote: "\u672A\u914D\u7F6E\u65F6 AI \u901A\u9053\u81EA\u52A8\u8DF3\u8FC7\uFF0C\u7531 Bing \u515C\u5E95\u3002",
1911
1362
  apiKeyTitle: "API Key",
1912
1363
  apiKeyDesc: "\u4FDD\u5B58\u81F3 {home} \u7684 {ref}\uFF0C\u4FDD\u5B58\u540E\u7ACB\u5373\u751F\u6548\uFF1B\u7559\u7A7A\u4FDD\u5B58 = \u6E05\u9664",
1913
1364
  apiKeyPlaceholderConfigured: "\u5DF2\u914D\u7F6E\uFF08\u5982\u9700\u66F4\u6362\u8BF7\u76F4\u63A5\u8F93\u5165\uFF09",
1914
1365
  save: "\u4FDD\u5B58",
1915
1366
  saving: "\u4FDD\u5B58\u4E2D\u2026",
1916
1367
  keySaved: "\u5DF2\u4FDD\u5B58\u5230 {home}\uFF0C\u7ACB\u5373\u751F\u6548",
1917
- keyCleared: "\u5DF2\u6E05\u9664 API Key\uFF08AI \u901A\u9053\u5C06\u4E0D\u53EF\u7528\uFF0CBing \u515C\u5E95\uFF09",
1368
+ keyCleared: "\u5DF2\u6E05\u9664 API Key\uFF08\u672A\u914D\u7F6E\u65F6\u6B63\u6587\u4FDD\u6301\u539F\u6587\uFF0C\u4E0D\u4F1A\u7FFB\u8BD1\uFF09",
1918
1369
  saveFailed: "\u4FDD\u5B58\u5931\u8D25\uFF1A{error}",
1919
1370
  unknownError: "\u672A\u77E5\u9519\u8BEF",
1920
1371
  baseUrlTitle: "Base URL",
1921
1372
  baseUrlDesc: "\u4EFB\u610F OpenAI \u517C\u5BB9\u670D\u52A1\u7AEF\u70B9\uFF0C\u5982 {url1}\u3001{url2}",
1922
1373
  modelTitle: "\u6A21\u578B",
1923
1374
  modelDesc: "\u5982 {model1}\u3001{model2}\uFF1B\u7559\u7A7A\u89C6\u4E3A\u672A\u914D\u7F6E",
1375
+ timeoutTitle: "\u5355\u6B21\u8BF7\u6C42\u8D85\u65F6",
1376
+ timeoutDesc: "\u5355\u4F4D\u6BEB\u79D2\uFF0C\u8303\u56F4 500-900000\u3002\u4E00\u6761\u957F\u56DE\u7B54\u53EF\u80FD\u8981\u51E0\u5206\u949F\uFF0C\u9ED8\u8BA4 600000\u3002",
1924
1377
  test: "\u6D4B\u8BD5 AI \u901A\u9053",
1925
1378
  testing: "\u6D4B\u8BD5\u4E2D\u2026",
1926
1379
  testOk: "\u8FDE\u63A5\u6B63\u5E38\uFF0C\u5EF6\u8FDF {latency}ms",
1927
1380
  testFail: "\u5931\u8D25\uFF1A{error}",
1928
- thinkEnableTitle: "\u663E\u793A\u601D\u8003\u94FE\u7FFB\u8BD1\u6309\u94AE",
1929
- thinkEnableDesc: "\u5728\u5C55\u5F00\u7684\u601D\u8003\u5361\u7684\u300CThink\u300D\u53F3\u4FA7\u663E\u793A\u4E00\u4E2A\u6309\u94AE\uFF0C\u70B9\u51FB\u624D\u7FFB\u8BD1\u8FD9\u6761\u601D\u8003\u6B63\u6587\uFF08\u4E0D\u4F1A\u81EA\u52A8\u7FFB\u8BD1\uFF09\uFF1B\u6298\u53E0\u8D77\u6765\u7684\u5361\u7247\u6CA1\u6709\u6309\u94AE\uFF0C\u5C55\u5F00\u540E\u6309\u94AE\u624D\u51FA\u73B0\u3002\u7FFB\u8BD1\u4E2D\u6309\u94AE\u8F6C\u5708\uFF0C\u7FFB\u597D\u540E\u5B9E\u5FC3\u9AD8\u4EAE\uFF0C\u518D\u70B9\u4E00\u4E0B\u6574\u6761\u5207\u56DE\u539F\u6587\uFF08\u6B63\u6587\u91CC\u7684\u70B9\u51FB\u4E0D\u5207\u6362\uFF09\u3002\u4EE3\u7801\u5757\u4E0E\u6298\u53E0\u6458\u8981\u4E0D\u52A8\uFF0CBing \u901A\u9053\u4E0D\u53C2\u4E0E\u3002\u5173\u95ED\u6B64\u5F00\u5173\u4F1A\u64A4\u6389\u6309\u94AE\u5E76\u8FD8\u539F\u5DF2\u7FFB\u8BD1\u5185\u5BB9\u3002",
1930
- thinkTimeoutTitle: "\u601D\u8003\u94FE\u7FFB\u8BD1\u8D85\u65F6",
1931
- thinkTimeoutDesc: "\u5355\u4F4D\u6BEB\u79D2\uFF0C\u8303\u56F4 500-900000\u3002\u6574\u5757\u601D\u8003\u53EF\u80FD\u8981\u51E0\u5206\u949F\uFF0C\u9ED8\u8BA4 600000\u3002",
1932
- bingTitle: "Bing \u7F51\u9875\u7FFB\u8BD1\uFF08\u514D Key \u515C\u5E95\uFF09",
1933
- enableBing: "\u542F\u7528 Bing \u7FFB\u8BD1\u901A\u9053",
1934
- bingDesc: "\u5185\u7F6E\u514D Key \u7FFB\u8BD1\u901A\u9053\uFF0C\u65E0\u9700\u4EFB\u4F55\u914D\u7F6E\uFF0C\u53EA\u7FFB\u8BD1\u5DE5\u5177\u8C03\u7528\u6807\u9898\u3002AI \u672A\u914D\u7F6E\u6216\u8BF7\u6C42\u5931\u8D25\u65F6\u81EA\u52A8\u515C\u5E95\uFF1B\u601D\u8003\u94FE\u7FFB\u8BD1\u6C38\u8FDC\u4E0D\u8D70\u8FD9\u6761\u901A\u9053\u3002AI \u4E0E Bing \u540C\u65F6\u5173\u95ED\u5219\u5DE5\u5177\u8C03\u7528\u6807\u9898\u4E5F\u4E0D\u7FFB\u8BD1\u3002",
1935
- behaviorTitle: "\u901A\u9053\u884C\u4E3A",
1936
- behavior1: "AI \u5F00\u542F\u4E14\u5DF2\u914D\u7F6E \u2192 AI \u4F18\u5148\u7FFB\u8BD1\uFF0C\u5931\u8D25\u81EA\u52A8\u964D\u7EA7 Bing",
1937
- behavior2: "AI \u5F00\u542F\u4F46\u672A\u914D\u7F6E + Bing \u5F00\u542F \u2192 \u7531 Bing \u7FFB\u8BD1",
1938
- behavior3: "AI \u5F00\u542F\u4F46\u672A\u914D\u7F6E + Bing \u5173\u95ED \u2192 \u4E0D\u7FFB\u8BD1",
1939
- behavior4: "AI \u5173\u95ED + Bing \u5F00\u542F \u2192 \u76F4\u63A5\u4F7F\u7528 Bing",
1940
- behavior5: "AI \u5173\u95ED + Bing \u5173\u95ED \u2192 \u4E0D\u7FFB\u8BD1",
1941
- concurrencyTitle: "\u6700\u5927\u7FFB\u8BD1\u5E76\u53D1\u6570",
1942
- concurrencyDesc: "\u63A7\u5236\u89C6\u53E3\u6EDA\u52A8\u4E0E\u591A\u5DE5\u5177\u5361\u7247\u65F6\u7684\u6700\u5927\u5E76\u884C\u8BF7\u6C42\u6570\uFF08\u8303\u56F4 1-100\uFF0C\u63A8\u8350 3\uFF09\u3002"
1381
+ 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"
1943
1383
  };
1944
1384
  var en = {
1945
1385
  pageNav: "Reading",
1946
- title: "Chat translate",
1386
+ title: "Reply translate",
1947
1387
  masterTitle: "Translation master switch",
1948
1388
  enableTranslation: "Enable translation",
1949
- masterDesc: "Translates tool-call titles in the current session (e.g. {example}) into Chinese; click a translation to switch back to the original in place. Think text stays untranslated by default and is controlled by \u201Cshow think-chain translate button\u201D in the AI card: once on, a button appears at the right of every expanded think card and translates only when clicked. The collapsed summary line of a think card is never translated. Applies to the session you are viewing only; reply text is never translated.",
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.",
1950
1390
  aiTitle: "AI translation (OpenAI-compatible)",
1951
- enableAi: "Enable the AI translation channel",
1391
+ 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.",
1952
1392
  badgeConfigured: "Configured",
1953
1393
  badgeUnconfigured: "Not configured",
1954
- aiFallbackNote: "When unconfigured the AI channel is skipped and Bing takes over.",
1955
1394
  apiKeyTitle: "API Key",
1956
1395
  apiKeyDesc: "Saved as {ref} in {home}; takes effect immediately. Saving an empty value clears it.",
1957
1396
  apiKeyPlaceholderConfigured: "Configured (type a new value to replace it)",
1958
1397
  save: "Save",
1959
1398
  saving: "Saving\u2026",
1960
1399
  keySaved: "Saved to {home}; takes effect immediately",
1961
- keyCleared: "API Key cleared (the AI channel becomes unavailable and Bing takes over)",
1400
+ keyCleared: "API Key cleared (with nothing configured, replies stay in their original language)",
1962
1401
  saveFailed: "Save failed: {error}",
1963
1402
  unknownError: "Unknown error",
1964
1403
  baseUrlTitle: "Base URL",
1965
1404
  baseUrlDesc: "Any OpenAI-compatible endpoint, e.g. {url1} or {url2}",
1966
1405
  modelTitle: "Model",
1967
1406
  modelDesc: "e.g. {model1} or {model2}; empty counts as unconfigured",
1407
+ timeoutTitle: "Per-request timeout",
1408
+ timeoutDesc: "In milliseconds, range 500-900000. One long reply can take minutes; default 600000.",
1968
1409
  test: "Test the AI channel",
1969
1410
  testing: "Testing\u2026",
1970
1411
  testOk: "Connected, {latency}ms latency",
1971
1412
  testFail: "Failed: {error}",
1972
- thinkEnableTitle: "Show the think-chain translate button",
1973
- thinkEnableDesc: "Shows a button to the right of \u201CThink\u201D on every expanded think card; it translates that think text only when clicked, never automatically. Collapsed cards carry no button; it appears once the card is expanded. While translating, the button spins; once done it highlights solid, and clicking it again switches the whole block back to the original (clicks inside the text do not switch). Code blocks and the collapsed summary stay untouched, and the Bing channel is not involved. Turning this off removes the button and restores already translated content.",
1974
- thinkTimeoutTitle: "Think-chain translation timeout",
1975
- thinkTimeoutDesc: "In milliseconds, range 500-900000. A whole think block can take minutes; default 600000.",
1976
- bingTitle: "Bing web translation (key-free fallback)",
1977
- enableBing: "Enable the Bing translation channel",
1978
- bingDesc: "A built-in key-free channel that needs no configuration and translates tool-call titles only. It takes over automatically when AI is unconfigured or a request fails; think-chain translation never uses this channel. With both AI and Bing off, tool-call titles are not translated either.",
1979
- behaviorTitle: "Channel behavior",
1980
- behavior1: "AI on and configured \u2192 AI translates first, falling back to Bing on failure",
1981
- behavior2: "AI on but unconfigured + Bing on \u2192 Bing translates",
1982
- behavior3: "AI on but unconfigured + Bing off \u2192 nothing is translated",
1983
- behavior4: "AI off + Bing on \u2192 Bing is used directly",
1984
- behavior5: "AI off + Bing off \u2192 nothing is translated",
1985
- concurrencyTitle: "Max translation concurrency",
1986
- concurrencyDesc: "Caps parallel requests while the viewport scrolls across multiple tool cards (range 1-100, recommended 3)."
1413
+ 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."
1987
1415
  };
1988
1416
 
1989
1417
  // src/client/settings/ui.tsx
@@ -2056,201 +1484,131 @@ function TidySettingsPanel(props) {
2056
1484
  }
2057
1485
  )
2058
1486
  ] }),
2059
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-tidy-desc", children: withSamples(t("masterDesc"), { example: "Locate DSH home directory structure" }) })
1487
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-tidy-desc", children: withSamples(t("masterDesc"), { example: "I traced the failing path to a stale lock file." }) })
2060
1488
  ] }),
2061
1489
  state.enabled && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
2062
1490
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-card", children: [
2063
1491
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-title", children: [
2064
1492
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: t("aiTitle") }),
2065
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
2066
- import_dsh_client_ui_primitives.Switch,
2067
- {
2068
- checked: state.aiEnabled,
2069
- onChange: (next) => settingsStore.update({ aiEnabled: next }),
2070
- label: t("enableAi")
2071
- }
2072
- )
1493
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.Tag, { tone: state.aiConfigured ? "success" : "warning", children: t(state.aiConfigured ? "badgeConfigured" : "badgeUnconfigured") })
2073
1494
  ] }),
2074
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-desc", children: [
2075
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(import_dsh_client_ui_primitives.Tag, { tone: state.aiConfigured ? "success" : "warning", children: t(state.aiConfigured ? "badgeConfigured" : "badgeUnconfigured") }),
2076
- " ",
2077
- t("aiFallbackNote")
2078
- ] }),
2079
- state.aiEnabled && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(import_jsx_runtime.Fragment, { children: [
2080
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-row", children: [
2081
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-row-info", children: [
2082
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-tidy-row-title", children: t("apiKeyTitle") }),
2083
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-tidy-row-desc", children: withSamples(t("apiKeyDesc"), {
2084
- home: CREDENTIALS_PATH,
2085
- ref: TRANSLATE_API_KEY_REF
2086
- }) })
2087
- ] }),
2088
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-input-row", children: [
2089
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
2090
- import_dsh_client_ui_primitives.Input,
2091
- {
2092
- type: "password",
2093
- className: "dsh-tidy-field-wide",
2094
- placeholder: state.aiConfigured ? t("apiKeyPlaceholderConfigured") : "sk-...",
2095
- value: apiKeyInput,
2096
- onChange: (e) => setApiKeyInput(e.target.value),
2097
- "aria-label": t("apiKeyTitle")
2098
- }
2099
- ),
2100
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
2101
- import_dsh_client_ui_primitives.Button,
2102
- {
2103
- variant: "primary",
2104
- size: "sm",
2105
- disabled: savingKey,
2106
- onClick: handleSaveKey,
2107
- children: savingKey ? t("saving") : t("save")
2108
- }
2109
- )
2110
- ] })
2111
- ] }),
2112
- keyMsg && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: `dsh-tidy-test-result ${keyMsg.ok ? "ok" : "fail"}`, children: keyMsg.text }),
2113
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-row", children: [
2114
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-row-info", children: [
2115
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-tidy-row-title", children: t("baseUrlTitle") }),
2116
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-tidy-row-desc", children: withSamples(t("baseUrlDesc"), {
2117
- url1: "https://api.openai.com/v1",
2118
- url2: "https://api.deepseek.com/v1"
2119
- }) })
2120
- ] }),
2121
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
2122
- import_dsh_client_ui_primitives.Input,
2123
- {
2124
- type: "text",
2125
- className: "dsh-tidy-field-wide",
2126
- placeholder: "https://api.openai.com/v1",
2127
- value: state.baseUrl,
2128
- onChange: (e) => settingsStore.update({ baseUrl: e.target.value }),
2129
- "aria-label": t("baseUrlTitle")
2130
- }
2131
- )
1495
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-tidy-desc", children: t("aiRule") }),
1496
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-row", children: [
1497
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-row-info", children: [
1498
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-tidy-row-title", children: t("apiKeyTitle") }),
1499
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-tidy-row-desc", children: withSamples(t("apiKeyDesc"), {
1500
+ home: CREDENTIALS_PATH,
1501
+ ref: TRANSLATE_API_KEY_REF
1502
+ }) })
2132
1503
  ] }),
2133
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-row", children: [
2134
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-row-info", children: [
2135
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-tidy-row-title", children: t("modelTitle") }),
2136
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-tidy-row-desc", children: withSamples(t("modelDesc"), {
2137
- model1: "gpt-4o-mini",
2138
- model2: "deepseek-chat"
2139
- }) })
2140
- ] }),
1504
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-input-row", children: [
2141
1505
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
2142
1506
  import_dsh_client_ui_primitives.Input,
2143
1507
  {
2144
- type: "text",
1508
+ type: "password",
2145
1509
  className: "dsh-tidy-field-wide",
2146
- placeholder: "gpt-4o-mini",
2147
- value: state.model,
2148
- onChange: (e) => settingsStore.update({ model: e.target.value }),
2149
- "aria-label": t("modelTitle")
1510
+ placeholder: state.aiConfigured ? t("apiKeyPlaceholderConfigured") : "sk-...",
1511
+ value: apiKeyInput,
1512
+ onChange: (e) => setApiKeyInput(e.target.value),
1513
+ "aria-label": t("apiKeyTitle")
2150
1514
  }
2151
- )
2152
- ] }),
2153
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-row", children: [
1515
+ ),
2154
1516
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
2155
1517
  import_dsh_client_ui_primitives.Button,
2156
1518
  {
2157
- variant: "outline",
1519
+ variant: "primary",
2158
1520
  size: "sm",
2159
- disabled: testing?.running,
2160
- onClick: () => runTest("openai"),
2161
- children: testing?.running ? t("testing") : t("test")
2162
- }
2163
- ),
2164
- testing?.channel === "openai" && !testing.running && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: `dsh-tidy-test-result ${testing.ok ? "ok" : "fail"}`, children: testing.message })
2165
- ] }),
2166
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-row", children: [
2167
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-row-info", children: [
2168
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-tidy-row-title", children: t("thinkEnableTitle") }),
2169
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-tidy-row-desc", children: t("thinkEnableDesc") })
2170
- ] }),
2171
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
2172
- import_dsh_client_ui_primitives.Switch,
2173
- {
2174
- checked: state.thinkEnabled,
2175
- onChange: (next) => settingsStore.update({ thinkEnabled: next }),
2176
- label: t("thinkEnableTitle")
2177
- }
2178
- )
2179
- ] }),
2180
- state.thinkEnabled && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-row", children: [
2181
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-row-info", children: [
2182
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-tidy-row-title", children: t("thinkTimeoutTitle") }),
2183
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-tidy-row-desc", children: t("thinkTimeoutDesc") })
2184
- ] }),
2185
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
2186
- import_dsh_client_ui_primitives.Input,
2187
- {
2188
- type: "number",
2189
- className: "dsh-tidy-field-timeout",
2190
- min: 500,
2191
- max: 9e5,
2192
- step: 1e3,
2193
- value: state.thinkTimeoutMs,
2194
- onChange: (e) => {
2195
- const val = parseInt(e.target.value, 10);
2196
- if (!Number.isFinite(val)) return;
2197
- settingsStore.update({
2198
- thinkTimeoutMs: Math.min(Math.max(val, 500), 9e5)
2199
- });
2200
- },
2201
- "aria-label": t("thinkTimeoutTitle")
1521
+ disabled: savingKey,
1522
+ onClick: handleSaveKey,
1523
+ children: savingKey ? t("saving") : t("save")
2202
1524
  }
2203
1525
  )
2204
1526
  ] })
2205
- ] })
2206
- ] }),
2207
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-card", children: [
2208
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-title", children: [
2209
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { children: t("bingTitle") }),
1527
+ ] }),
1528
+ keyMsg && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: `dsh-tidy-test-result ${keyMsg.ok ? "ok" : "fail"}`, children: keyMsg.text }),
1529
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-row", children: [
1530
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-row-info", children: [
1531
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-tidy-row-title", children: t("baseUrlTitle") }),
1532
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-tidy-row-desc", children: withSamples(t("baseUrlDesc"), {
1533
+ url1: "https://api.openai.com/v1",
1534
+ url2: "https://api.deepseek.com/v1"
1535
+ }) })
1536
+ ] }),
1537
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1538
+ import_dsh_client_ui_primitives.Input,
1539
+ {
1540
+ type: "text",
1541
+ className: "dsh-tidy-field-wide",
1542
+ placeholder: "https://api.openai.com/v1",
1543
+ value: state.baseUrl,
1544
+ onChange: (e) => settingsStore.update({ baseUrl: e.target.value }),
1545
+ "aria-label": t("baseUrlTitle")
1546
+ }
1547
+ )
1548
+ ] }),
1549
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-row", children: [
1550
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-row-info", children: [
1551
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-tidy-row-title", children: t("modelTitle") }),
1552
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-tidy-row-desc", children: withSamples(t("modelDesc"), {
1553
+ model1: "gpt-4o-mini",
1554
+ model2: "deepseek-chat"
1555
+ }) })
1556
+ ] }),
1557
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1558
+ import_dsh_client_ui_primitives.Input,
1559
+ {
1560
+ type: "text",
1561
+ className: "dsh-tidy-field-wide",
1562
+ placeholder: "gpt-4o-mini",
1563
+ value: state.model,
1564
+ onChange: (e) => settingsStore.update({ model: e.target.value }),
1565
+ "aria-label": t("modelTitle")
1566
+ }
1567
+ )
1568
+ ] }),
1569
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-row", children: [
1570
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-row-info", children: [
1571
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-tidy-row-title", children: t("timeoutTitle") }),
1572
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-tidy-row-desc", children: t("timeoutDesc") })
1573
+ ] }),
2210
1574
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
2211
- import_dsh_client_ui_primitives.Switch,
1575
+ import_dsh_client_ui_primitives.Input,
2212
1576
  {
2213
- checked: state.bingEnabled,
2214
- onChange: (next) => settingsStore.update({ bingEnabled: next }),
2215
- label: t("enableBing")
1577
+ type: "number",
1578
+ className: "dsh-tidy-field-timeout",
1579
+ min: 500,
1580
+ max: 9e5,
1581
+ step: 1e3,
1582
+ value: state.aiTimeoutMs,
1583
+ onChange: (e) => {
1584
+ const val = parseInt(e.target.value, 10);
1585
+ if (!Number.isFinite(val)) return;
1586
+ settingsStore.update({
1587
+ aiTimeoutMs: Math.min(Math.max(val, 500), 9e5)
1588
+ });
1589
+ },
1590
+ "aria-label": t("timeoutTitle")
2216
1591
  }
2217
1592
  )
2218
1593
  ] }),
2219
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-tidy-desc", children: t("bingDesc") })
1594
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-row", children: [
1595
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
1596
+ import_dsh_client_ui_primitives.Button,
1597
+ {
1598
+ variant: "outline",
1599
+ size: "sm",
1600
+ disabled: testing?.running,
1601
+ onClick: () => runTest("openai"),
1602
+ children: testing?.running ? t("testing") : t("test")
1603
+ }
1604
+ ),
1605
+ testing?.channel === "openai" && !testing.running && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: `dsh-tidy-test-result ${testing.ok ? "ok" : "fail"}`, children: testing.message })
1606
+ ] })
2220
1607
  ] }),
2221
1608
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-card", children: [
2222
1609
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-tidy-row-title", children: t("behaviorTitle") }),
2223
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("ul", { className: "dsh-tidy-desc dsh-tidy-behavior-list", children: [
2224
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { children: t("behavior1") }),
2225
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { children: t("behavior2") }),
2226
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { children: t("behavior3") }),
2227
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { children: t("behavior4") }),
2228
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("li", { children: t("behavior5") })
2229
- ] })
2230
- ] }),
2231
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-tidy-card", children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-row", children: [
2232
- /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "dsh-tidy-row-info", children: [
2233
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-tidy-row-title", children: t("concurrencyTitle") }),
2234
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-tidy-row-desc", children: t("concurrencyDesc") })
2235
- ] }),
2236
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
2237
- import_dsh_client_ui_primitives.Input,
2238
- {
2239
- type: "number",
2240
- className: "dsh-tidy-field-count",
2241
- min: 1,
2242
- max: 100,
2243
- step: 1,
2244
- value: state.concurrency,
2245
- onChange: (e) => {
2246
- const val = parseInt(e.target.value, 10);
2247
- if (!Number.isFinite(val)) return;
2248
- settingsStore.update({ concurrency: Math.min(Math.max(val, 1), 100) });
2249
- },
2250
- "aria-label": t("concurrencyTitle")
2251
- }
2252
- )
2253
- ] }) })
1610
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "dsh-tidy-desc", children: t("behaviorRule") })
1611
+ ] })
2254
1612
  ] })
2255
1613
  ] });
2256
1614
  }
@@ -2312,7 +1670,7 @@ function setupSettingsUi(ctx) {
2312
1670
  var name = "dsh-chat-translate";
2313
1671
  var inject = ["slots", "configForms", "remote", "remote.credentials", "locale"];
2314
1672
  function apply(ctx) {
2315
- ctx.effect(() => chatTranslateObserver.start(document), "dsh-chat-translate: title translate observer");
1673
+ ctx.effect(() => chatTranslateObserver.start(document), "dsh-chat-translate: reply translate observer");
2316
1674
  ctx.effect(() => setupSettingsUi(ctx), "dsh-chat-translate: settings section");
2317
1675
  }
2318
1676
  return module.exports; } });