dsh-think-translate 1.2.0 → 1.2.2

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/README.de.md CHANGED
@@ -33,7 +33,7 @@ Modelle der DeepSeek-Familie denken oft auf Chinesisch — oder in der Sprache,
33
33
  - **🎚️ Einstellbarer Übersetzungszeitpunkt** — alles vorübersetzen / alte Ketten lazy laden (Standard) / nur beim Aufklappen
34
34
  - **🔗 Dynamische Anbieter-Kette** — die Listenreihenfolge ist die Ausführungsreihenfolge: ziehen zum Sortieren, jede Zeile einzeln ein-/ausschalten. Integriert google gtx / bing / lokales Ollama plus beliebige eigene Endpunkte
35
35
  - **🔌 Eigene Anbieter (OpenAI & Anthropic)** — im Panel jeden OpenAI-kompatiblen Endpunkt (`/v1/chat/completions`) oder die **Anthropic Messages API** (Claude) hinzufügen: Typ, Vorlage, Basis-URL, API-Schlüssel, Modell
36
- - **🪄 DSH-konfigurierte Anbieter übernehmen** — liest schreibgeschützte DSH-Zeilen aus `settings.yaml` (`llm-pi-ai.providers`); eine Schaltfläche liest neu ein und fügt alle der Kette hinzu. Der Schlüssel wird pro Anfrage aus `.credentials.yaml` aufgelöst und nie in der Plugin-Konfiguration gespeichert
36
+ - **🪄 DSH-konfigurierte Anbieter übernehmen** — liest schreibgeschützte DSH-Zeilen aus `settings.yaml` (`llm-pi-ai.providers`); eine Schaltfläche liest neu ein und fügt alle der Kette hinzu. Der Schlüssel wird pro Anfrage aus `.credentials.yaml` aufgelöst und nie in der Plugin-Konfiguration gespeichert. Auch die eigene Standardroute des Harness zählt: zeigt `agent-default-model` auf `deepseek-official`, erscheint die offizielle DeepSeek-API als weitere DSH-Zeile
37
37
  - **⏱️ Robust** — 3 Wiederholungen mit Backoff + direkter Browser-Fallback, Test-Schaltfläche pro Zeile, Fehlschläge werden nie zwischengespeichert
38
38
 
39
39
  ## 📦 Installation
package/README.es.md CHANGED
@@ -33,7 +33,7 @@ Los modelos de la familia DeepSeek suelen razonar en chino — o en el idioma en
33
33
  - **🎚️ Momento de traducción ajustable** — pre-traducir todo / carga diferida de cadenas antiguas (por defecto) / solo al expandir
34
34
  - **🔗 Cadena de proveedores dinámica** — el orden de la lista es el orden de uso: arrastra para reordenar y activa o desactiva cada fila. Integrados google gtx / bing / Ollama local más cualquier endpoint personalizado
35
35
  - **🔌 Proveedores personalizados (OpenAI y Anthropic)** — añade desde el panel cualquier endpoint compatible con OpenAI (`/v1/chat/completions`) o la **Anthropic Messages API** (Claude): tipo, preajuste, URL base, clave de API y modelo
36
- - **🪄 Hereda los proveedores configurados en DSH** — detecta filas DSH de solo lectura desde `settings.yaml` (`llm-pi-ai.providers`) y un botón reescanea y las añade todas a la cadena; la clave se resuelve en cada petición desde `.credentials.yaml` y nunca se guarda en la config del plugin
36
+ - **🪄 Hereda los proveedores configurados en DSH** — detecta filas DSH de solo lectura desde `settings.yaml` (`llm-pi-ai.providers`) y un botón reescanea y las añade todas a la cadena; la clave se resuelve en cada petición desde `.credentials.yaml` y nunca se guarda en la config del plugin. La ruta por defecto del propio harness también cuenta: si `agent-default-model` apunta a `deepseek-official`, la API oficial de DeepSeek aparece como una fila DSH más
37
37
  - **⏱️ Resistente** — 3 reintentos con backoff + respaldo directo del navegador, botón de prueba por fila, los fallos nunca se almacenan en caché
38
38
 
39
39
  ## 📦 Instalación
package/README.fr.md CHANGED
@@ -33,7 +33,7 @@ Les modèles de la famille DeepSeek raisonnent souvent en chinois — ou dans la
33
33
  - **🎚️ Moment de traduction réglable** — tout pré-traduire / chargement différé des anciennes chaînes (défaut) / uniquement à l'ouverture
34
34
  - **🔗 Chaîne de fournisseurs dynamique** — l'ordre de la liste est l'ordre d'exécution : glissez pour réordonner, activez ou désactivez chaque ligne. Intégrés google gtx / bing / Ollama local, plus tout endpoint personnalisé
35
35
  - **🔌 Fournisseurs personnalisés (OpenAI et Anthropic)** — ajoutez depuis le panneau tout endpoint compatible OpenAI (`/v1/chat/completions`) ou l'**Anthropic Messages API** (Claude) : type, préréglage, URL de base, clé API et modèle
36
- - **🪄 Hérite des fournisseurs configurés dans DSH** — détecte les lignes DSH en lecture seule depuis `settings.yaml` (`llm-pi-ai.providers`), et un bouton relance l'analyse et les ajoute toutes à la chaîne ; la clé est résolue à chaque requête depuis `.credentials.yaml` et n'est jamais écrite dans la config du plugin
36
+ - **🪄 Hérite des fournisseurs configurés dans DSH** — détecte les lignes DSH en lecture seule depuis `settings.yaml` (`llm-pi-ai.providers`), et un bouton relance l'analyse et les ajoute toutes à la chaîne ; la clé est résolue à chaque requête depuis `.credentials.yaml` et n'est jamais écrite dans la config du plugin. La route par défaut du harness compte aussi : si `agent-default-model` désigne `deepseek-official`, l'API officielle DeepSeek apparaît comme une ligne DSH de plus
37
37
  - **⏱️ Résilient** — 3 tentatives avec backoff + secours direct du navigateur, bouton de test par ligne, les échecs ne sont jamais mis en cache
38
38
 
39
39
  ## 📦 Installation
package/README.ja.md CHANGED
@@ -33,7 +33,7 @@ DeepSeek 系モデルは中国語で考えることが多く、あるいはた
33
33
  - **🎚️ 翻訳タイミングを調整可能** — すべて事前翻訳 / 履歴を遅延ロード(既定)/ 展開時のみ翻訳
34
34
  - **🔗 動的プロバイダーチェーン** — 一覧の順序がそのまま実行順。ドラッグで並べ替え、行ごとにオン/オフ。組み込みの google gtx / bing / ローカル Ollama に加えて任意のカスタム端点
35
35
  - **🔌 カスタムプロバイダー(OpenAI / Anthropic)** — 設定パネルから任意の OpenAI 互換端点(`/v1/chat/completions`)や **Anthropic Messages API**(Claude)を追加:種類・プリセット・ベース URL・API キー・モデル
36
- - **🪄 DSH 設定のプロバイダーを引き継ぐ** — `settings.yaml`(`llm-pi-ai.providers`)から読み取り専用の DSH 行を自動検出し、ボタン1つで再スキャンしてまとめてチェーンに追加。キーは `.credentials.yaml` からリクエスト時に解決され、プラグインの設定には保存されません
36
+ - **🪄 DSH 設定のプロバイダーを引き継ぐ** — `settings.yaml`(`llm-pi-ai.providers`)から読み取り専用の DSH 行を自動検出し、ボタン1つで再スキャンしてまとめてチェーンに追加。キーは `.credentials.yaml` からリクエスト時に解決され、プラグインの設定には保存されません。harness 自身の既定ルートも対象です:`agent-default-model` が `deepseek-official` を指していれば、DeepSeek の公式 API も DSH 行として提供されます
37
37
  - **⏱️ 失敗に強い** — host は3回バックオフ再試行 + ブラウザ直接フォールバック、行ごとのテストボタン、失敗結果はキャッシュしない
38
38
 
39
39
  ## 📦 インストール
package/README.ko.md CHANGED
@@ -33,7 +33,7 @@ DeepSeek 계열 모델은 중국어로, 또는 우연히 생각에 쓰는 언어
33
33
  - **🎚️ 번역 시점 조절** — 모두 사전 번역 / 이전 체인 지연 로딩(기본)/ 펼친 체인만 번역
34
34
  - **🔗 동적 제공자 체인** — 목록 순서가 곧 실행 순서. 끌어서 정렬하고 행마다 켜고 끄기. 내장 google gtx / bing / 로컬 Ollama에 더해 임의의 사용자 지정 엔드포인트
35
35
  - **🔌 사용자 지정 제공자(OpenAI / Anthropic)** — 설정 패널에서 OpenAI 호환 엔드포인트(`/v1/chat/completions`)나 **Anthropic Messages API**(Claude)를 추가: 유형, 프리셋, 베이스 URL, API 키, 모델
36
- - **🪄 DSH 설정의 제공자 상속** — `settings.yaml`(`llm-pi-ai.providers`)에서 읽기 전용 DSH 행을 자동 발견하고, 버튼 한 번으로 다시 검사해 모두 체인에 추가. 키는 요청 시 `.credentials.yaml`에서 해석되며 플러그인 설정에 저장되지 않음
36
+ - **🪄 DSH 설정의 제공자 상속** — `settings.yaml`(`llm-pi-ai.providers`)에서 읽기 전용 DSH 행을 자동 발견하고, 버튼 한 번으로 다시 검사해 모두 체인에 추가. 키는 요청 시 `.credentials.yaml`에서 해석되며 플러그인 설정에 저장되지 않음. harness 자체의 기본 경로도 포함됩니다: `agent-default-model`이 `deepseek-official`을 가리키면 DeepSeek 공식 API도 DSH 행으로 제공됩니다
37
37
  - **⏱️ 실패에 강함** — host 3회 백오프 재시도 + 브라우저 직접 폴백, 행별 테스트 버튼, 실패 결과는 캐시하지 않음
38
38
 
39
39
  ## 📦 설치
package/README.md CHANGED
@@ -24,7 +24,7 @@ DeepSeek-class models often reason in Chinese — or in whatever language they h
24
24
  - **🌍 8 languages, one consistent UI** — 中文 / English / 日本語 / 한국어 / Español / Français / Deutsch / Русский; the settings panel, thinking rows and task cards all follow your choice, and it persists across reloads
25
25
  - **🔗 Dynamic provider chain** — order providers by drag-and-drop and enable/disable each one. Built-ins (google gtx, bing, local Ollama) plus any number of custom providers; the chain already ends with the free providers, so a second fallback pass is a config-file option, not a UI one
26
26
  - **🔌 Custom providers (OpenAI & Anthropic)** — add arbitrary OpenAI-compatible endpoints (any `/v1/chat/completions` gateway) or native **Anthropic Messages API** endpoints (Claude) from the settings panel: name, type, base URL, API key, model
27
- - **🪄 DSH provider discovery** — providers already configured in DSH's `settings.yaml` (`llm-pi-ai.providers`, e.g. linuxdo-hub, coding-hub) are auto-discovered and appear in the chain as read-only "DSH" entries, and **one button in settings rescans and adds them all at once**; keys are resolved from `.credentials.yaml` at runtime and never written to the plugin's config
27
+ - **🪄 DSH provider discovery** — providers already configured in DSH's `settings.yaml` (`llm-pi-ai.providers`, e.g. linuxdo-hub, coding-hub) are auto-discovered and appear in the chain as read-only "DSH" entries, and **one button in settings rescans and adds them all at once**; keys are resolved from `.credentials.yaml` at runtime and never written to the plugin's config. The harness's own default route counts too: when `agent-default-model` points at `deepseek-official`, DeepSeek's API is offered as one more DSH entry
28
28
  - **🔒 Private & offline-first** — local Ollama (qwen2.5:7b / 14b or custom) is a first-class provider: free, unlimited, nothing leaves your machine. First local-model selection **auto-downloads** the model with a live progress bar and enables it when done
29
29
  - **🧠 Zero context cost** — pure display layer: the model still sees the original text, and translated text never consumes the context window
30
30
  - **☁️ Google / Bing fallback** — automatic switch when other providers are unavailable (google goes through a Node CONNECT tunnel using the system proxy, bypassing anti-bot blocks)
package/README.ru.md CHANGED
@@ -33,7 +33,7 @@
33
33
  - **🎚️ Настраиваемый момент перевода** — переводить всё / ленивая загрузка старых цепочек (по умолчанию) / только при раскрытии
34
34
  - **🔗 Динамическая цепочка провайдеров** — порядок списка и есть порядок обращения: перетаскивайте для сортировки, включайте и выключайте каждую строку. Встроенные google gtx / bing / локальный Ollama плюс любые свои эндпоинты
35
35
  - **🔌 Свои провайдеры (OpenAI и Anthropic)** — из панели можно добавить любой OpenAI-совместимый эндпоинт (`/v1/chat/completions`) или **Anthropic Messages API** (Claude): тип, шаблон, базовый URL, ключ API, модель
36
- - **🪄 Наследование провайдеров из DSH** — читает из `settings.yaml` (`llm-pi-ai.providers`) строки DSH только для чтения; одна кнопка заново сканирует и добавляет их все в цепочку. Ключ разрешается на каждый запрос из `.credentials.yaml` и никогда не попадает в конфиг плагина
36
+ - **🪄 Наследование провайдеров из DSH** — читает из `settings.yaml` (`llm-pi-ai.providers`) строки DSH только для чтения; одна кнопка заново сканирует и добавляет их все в цепочку. Ключ разрешается на каждый запрос из `.credentials.yaml` и никогда не попадает в конфиг плагина. Собственный маршрут харнесса тоже учитывается: если `agent-default-model` указывает на `deepseek-official`, официальный API DeepSeek появляется как ещё одна строка DSH
37
37
  - **⏱️ Устойчивость** — 3 повтора с backoff + прямой запрос из браузера, кнопка проверки в каждой строке, ошибки не кэшируются
38
38
 
39
39
  ## 📦 Установка
package/README.zh-CN.md CHANGED
@@ -33,7 +33,7 @@ DeepSeek 系模型经常用中文思考——或者用它们碰巧习惯的语
33
33
  - **🎚️ 可调翻译时机** — 三档:全部预翻译 / 懒加载历史(默认)/ 仅展开时翻译
34
34
  - **🔗 动态提供方链** — 列表顺序就是投递顺序:拖动排序、逐行开关;内置 google gtx / bing / 本地 Ollama,外加任意数量的自定义端点
35
35
  - **🔌 自定义提供方(OpenAI 与 Anthropic)** — 设置面板里可添加任意 OpenAI 兼容端点(任何 `/v1/chat/completions` 网关)或原生 **Anthropic Messages API**(Claude):类型、预设、接口地址、API 密钥、模型
36
- - **🪄 继承 DSH 已配置的提供方** — 自动发现 `settings.yaml`(`llm-pi-ai.providers`)里的只读 DSH 行,一个按钮重新扫描并全部加入链;密钥在请求时从 `.credentials.yaml` 解析,绝不写入插件配置
36
+ - **🪄 继承 DSH 已配置的提供方** — 自动发现 `settings.yaml`(`llm-pi-ai.providers`)里的只读 DSH 行,一个按钮重新扫描并全部加入链;密钥在请求时从 `.credentials.yaml` 解析,绝不写入插件配置;harness 自己的默认路线也算:`agent-default-model` 指向 `deepseek-official` 时,DeepSeek 官方 API 也会作为一条 DSH 条目提供
37
37
  - **⏱️ 失败韧性** — host 请求 3 次退避重试 + 浏览器直连兜底、每行独立的测试按钮、失败结果不缓存
38
38
 
39
39
  ## 📦 安装
package/lib/client.js CHANGED
@@ -23,7 +23,7 @@ window.__ModuleLoader__.load({
23
23
  // there yet (an older process still serving the port), and the contract test
24
24
  // pins PLUGIN_VERSION to package.json so the two can never drift.
25
25
  var PLUGIN_NAME = "dsh-think-translate";
26
- var PLUGIN_VERSION = "1.2.0";
26
+ var PLUGIN_VERSION = "1.2.2";
27
27
  var PLUGIN_REPO = "https://github.com/UncleK/dsh-think-translate";
28
28
  // The official MarkdownText contract changed in 0.1.2-alpha: it now takes a
29
29
  // nested `labels` object and dereferences it without defaults inside its code
@@ -142,7 +142,7 @@ window.__ModuleLoader__.load({
142
142
  ".xl-provider-row:active{opacity:.55}",
143
143
  ".xl-provider-type{font-size:11px;opacity:.6;flex:0 0 auto}",
144
144
  ".xl-provider-name{font-size:12px;font-weight:500;flex:1 1 auto;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
145
- ".xl-badge{font-size:9px;padding:1px 5px;border-radius:4px;background:var(--dsw-alias-brand-primary,#4c9aff);color:#fff;font-weight:600}",
145
+ ".xl-badge{font-size:10px;line-height:16px;padding:0 6px;border-radius:999px;border:1px solid var(--dsw-alias-border-l2,rgba(127,127,127,.45));background:transparent;color:inherit;opacity:.7;font-weight:500;white-space:nowrap}",
146
146
  ".xl-add-form{display:flex;flex-direction:column;gap:6px;padding:8px;background:var(--dsw-alias-bg-layer-2,rgba(127,127,127,.08));border-radius:8px;margin:4px 0}",
147
147
  ".xl-form-row{display:flex;align-items:center;gap:8px}",
148
148
  ".xl-form-label{flex:0 0 62px;font-size:11.5px;opacity:.65}",
@@ -155,7 +155,6 @@ window.__ModuleLoader__.load({
155
155
  ".xl-menu-item:hover{background:var(--dsw-alias-bg-layer-2,rgba(127,127,127,.14))}",
156
156
  ".xl-menu-item-active{color:var(--dsw-alias-brand-primary,#4c9aff);font-weight:600}",
157
157
  ".xl-menu-note{font-size:11px;opacity:.55;padding:4px 8px 2px;margin-top:2px;border-top:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.2))}",
158
- ".xl-dsh-menu{margin:4px 0;padding:6px;border:1px solid var(--dsw-alias-border-l1,rgba(127,127,127,.35));border-radius:8px;background:var(--dsw-alias-bg-layer-2,rgba(127,127,127,.06))}",
159
158
  ".xl-caret:disabled{opacity:.35;cursor:default}",
160
159
  ".xl-unused{opacity:.85}",
161
160
  ".xl-kv-value{min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}",
@@ -170,6 +169,7 @@ window.__ModuleLoader__.load({
170
169
  ".xl-model-item-active{border-color:var(--dsw-alias-brand-primary,#4c9aff);color:var(--dsw-alias-brand-primary,#4c9aff)}",
171
170
  ".xl-x-btn{border:0;background:none;color:inherit;opacity:.5;cursor:pointer;font-size:15px;line-height:1;padding:0 6px;border-radius:4px}",
172
171
  ".xl-x-btn:hover{opacity:1;background:var(--dsw-alias-bg-layer-2,rgba(127,127,127,.12))}",
172
+ ".xl-add-btn{padding:1px 7px;font-size:14px;line-height:1.35;font-weight:600}",
173
173
  ".xl-provider-row .xl-sel{max-width:260px}",
174
174
  ".xl-model-row{display:flex;align-items:center;gap:6px;flex-wrap:wrap}",
175
175
  ".xl-model-row .xl-sel{max-width:220px}",
@@ -260,9 +260,12 @@ window.__ModuleLoader__.load({
260
260
  providerBaseURL: "接口地址",
261
261
  providerApiKey: "API 密钥",
262
262
  footnotes: "脚注",
263
- dshImport: "继承 DSH 已配置的 API",
263
+ dshImport: "重新扫描 DSH 配置",
264
264
  dshImportAll: "全部加入链",
265
265
  dshImportEmpty: "DSH 里没有发现已配置的 provider",
266
+ dshRescanning: "正在重新扫描 DSH 配置…",
267
+ dshRescanDone: "重新扫描完成",
268
+ providerDshList: "DSH 已配置提供方",
266
269
  starCta: "给个星星鼓励一下",
267
270
  providerPreset: "提供方",
268
271
  currentInUse: "当前使用",
@@ -281,7 +284,7 @@ window.__ModuleLoader__.load({
281
284
  providerOff: "关闭",
282
285
  providerSaved: "已保存",
283
286
  providerDeleted: "已删除",
284
- providerDrag: "拖动排序",
287
+ providerDrag: "翻译优先级(拖动排序)",
285
288
  providerCancel: "取消",
286
289
  providerSave: "保存",
287
290
  providerSaveError: "保存失败",
@@ -308,9 +311,12 @@ window.__ModuleLoader__.load({
308
311
  providerBaseURL: "Base URL",
309
312
  providerApiKey: "API key",
310
313
  footnotes: "Footnotes",
311
- dshImport: "Import from DSH config",
314
+ dshImport: "Rescan DSH config",
312
315
  dshImportAll: "Add all to chain",
313
316
  dshImportEmpty: "No provider configured in DSH",
317
+ dshRescanning: "Rescanning the DSH config…",
318
+ dshRescanDone: "Rescan complete",
319
+ providerDshList: "Configured in DSH",
314
320
  starCta: "Star it on GitHub",
315
321
  providerPreset: "Provider",
316
322
  currentInUse: "In use",
@@ -329,7 +335,7 @@ window.__ModuleLoader__.load({
329
335
  providerOff: "off",
330
336
  providerSaved: "Saved",
331
337
  providerDeleted: "Deleted",
332
- providerDrag: "Drag to reorder",
338
+ providerDrag: "Translation priority (drag to reorder)",
333
339
  providerCancel: "Cancel",
334
340
  providerSave: "Save",
335
341
  providerSaveError: "Save failed",
@@ -356,9 +362,12 @@ window.__ModuleLoader__.load({
356
362
  providerApiKey: "API キー",
357
363
  copy: "コピー", copied: "コピー済み",
358
364
  footnotes: "脚注",
359
- dshImport: "DSH の設定から取り込む",
365
+ dshImport: "DSH 設定を再スキャン",
360
366
  dshImportAll: "すべてチェーンに追加",
361
367
  dshImportEmpty: "DSH に設定済みプロバイダーがありません",
368
+ dshRescanning: "DSH 設定を再スキャン中…",
369
+ dshRescanDone: "再スキャン完了",
370
+ providerDshList: "DSH に設定済みの提供元",
362
371
  starCta: "GitHub でスターを",
363
372
  providerPreset: "提供元",
364
373
  currentInUse: "使用中",
@@ -377,7 +386,7 @@ window.__ModuleLoader__.load({
377
386
  providerOff: "オフ",
378
387
  providerSaved: "保存しました",
379
388
  providerDeleted: "削除しました",
380
- providerDrag: "ドラッグで並べ替え",
389
+ providerDrag: "翻訳の優先順位(ドラッグで並べ替え)",
381
390
  providerCancel: "キャンセル",
382
391
  providerSave: "保存",
383
392
  providerSaveError: "保存に失敗",
@@ -404,9 +413,12 @@ window.__ModuleLoader__.load({
404
413
  providerApiKey: "API 키",
405
414
  copy: "복사", copied: "복사됨",
406
415
  footnotes: "각주",
407
- dshImport: "DSH 설정에서 가져오기",
416
+ dshImport: "DSH 설정 다시 검사",
408
417
  dshImportAll: "모두 체인에 추가",
409
418
  dshImportEmpty: "DSH에 구성된 제공자가 없습니다",
419
+ dshRescanning: "DSH 설정을 다시 검사하는 중…",
420
+ dshRescanDone: "다시 검사 완료",
421
+ providerDshList: "DSH에 설정된 제공자",
410
422
  starCta: "GitHub에서 별 주기",
411
423
  providerPreset: "제공자",
412
424
  currentInUse: "사용 중",
@@ -425,7 +437,7 @@ window.__ModuleLoader__.load({
425
437
  providerOff: "끄기",
426
438
  providerSaved: "저장됨",
427
439
  providerDeleted: "삭제됨",
428
- providerDrag: "끌어서 순서 변경",
440
+ providerDrag: "번역 우선순위(끌어서 정렬)",
429
441
  providerCancel: "취소",
430
442
  providerSave: "저장",
431
443
  providerSaveError: "저장 실패",
@@ -452,9 +464,12 @@ window.__ModuleLoader__.load({
452
464
  providerApiKey: "Clave de API",
453
465
  copy: "Copiar", copied: "Copiado",
454
466
  footnotes: "Notas al pie",
455
- dshImport: "Importar desde la config de DSH",
467
+ dshImport: "Reescanear la config de DSH",
456
468
  dshImportAll: "Añadir todo a la cadena",
457
469
  dshImportEmpty: "No hay proveedores configurados en DSH",
470
+ dshRescanning: "Reescaneando la config de DSH…",
471
+ dshRescanDone: "Reescaneo completado",
472
+ providerDshList: "Configurados en DSH",
458
473
  starCta: "Danos una estrella",
459
474
  providerPreset: "Proveedor",
460
475
  currentInUse: "En uso",
@@ -473,7 +488,7 @@ window.__ModuleLoader__.load({
473
488
  providerOff: "Desactivado",
474
489
  providerSaved: "Guardado",
475
490
  providerDeleted: "Eliminado",
476
- providerDrag: "Arrastra para reordenar",
491
+ providerDrag: "Prioridad de traducción (arrastra para reordenar)",
477
492
  providerCancel: "Cancelar",
478
493
  providerSave: "Guardar",
479
494
  providerSaveError: "Error al guardar",
@@ -500,9 +515,12 @@ window.__ModuleLoader__.load({
500
515
  providerApiKey: "Clé API",
501
516
  copy: "Copier", copied: "Copié",
502
517
  footnotes: "Notes de bas de page",
503
- dshImport: "Importer depuis la config DSH",
518
+ dshImport: "Réanalyser la config DSH",
504
519
  dshImportAll: "Tout ajouter à la chaîne",
505
520
  dshImportEmpty: "Aucun fournisseur configuré dans DSH",
521
+ dshRescanning: "Réanalyse de la config DSH…",
522
+ dshRescanDone: "Réanalyse terminée",
523
+ providerDshList: "Configurés dans DSH",
506
524
  starCta: "Mettre une étoile",
507
525
  providerPreset: "Fournisseur",
508
526
  currentInUse: "En cours d'utilisation",
@@ -521,7 +539,7 @@ window.__ModuleLoader__.load({
521
539
  providerOff: "Désactivé",
522
540
  providerSaved: "Enregistré",
523
541
  providerDeleted: "Supprimé",
524
- providerDrag: "Glisser pour réordonner",
542
+ providerDrag: "Priorité de traduction (glisser pour réordonner)",
525
543
  providerCancel: "Annuler",
526
544
  providerSave: "Enregistrer",
527
545
  providerSaveError: "Échec de l'enregistrement",
@@ -548,9 +566,12 @@ window.__ModuleLoader__.load({
548
566
  providerApiKey: "API-Schlüssel",
549
567
  copy: "Kopieren", copied: "Kopiert",
550
568
  footnotes: "Fußnoten",
551
- dshImport: "Aus DSH-Konfiguration übernehmen",
569
+ dshImport: "DSH-Konfiguration neu einlesen",
552
570
  dshImportAll: "Alle zur Kette hinzufügen",
553
571
  dshImportEmpty: "Kein konfigurierter Anbieter in DSH",
572
+ dshRescanning: "DSH-Konfiguration wird neu eingelesen…",
573
+ dshRescanDone: "Neu eingelesen",
574
+ providerDshList: "In DSH konfiguriert",
554
575
  starCta: "Mit einem Stern danken",
555
576
  providerPreset: "Anbieter",
556
577
  currentInUse: "In Benutzung",
@@ -569,7 +590,7 @@ window.__ModuleLoader__.load({
569
590
  providerOff: "Aus",
570
591
  providerSaved: "Gespeichert",
571
592
  providerDeleted: "Gelöscht",
572
- providerDrag: "Zum Sortieren ziehen",
593
+ providerDrag: "Übersetzungspriorität (zum Sortieren ziehen)",
573
594
  providerCancel: "Abbrechen",
574
595
  providerSave: "Speichern",
575
596
  providerSaveError: "Speichern fehlgeschlagen",
@@ -596,9 +617,12 @@ window.__ModuleLoader__.load({
596
617
  providerApiKey: "Ключ API",
597
618
  copy: "Копировать", copied: "Скопировано",
598
619
  footnotes: "Сноски",
599
- dshImport: "Импортировать из конфигурации DSH",
620
+ dshImport: "Пересканировать конфиг DSH",
600
621
  dshImportAll: "Добавить все в цепочку",
601
622
  dshImportEmpty: "В DSH нет настроенных провайдеров",
623
+ dshRescanning: "Пересканирую конфиг DSH…",
624
+ dshRescanDone: "Сканирование завершено",
625
+ providerDshList: "Настроены в DSH",
602
626
  starCta: "Поставить звезду",
603
627
  providerPreset: "Провайдер",
604
628
  currentInUse: "Используется",
@@ -617,7 +641,7 @@ window.__ModuleLoader__.load({
617
641
  providerOff: "Выкл",
618
642
  providerSaved: "Сохранено",
619
643
  providerDeleted: "Удалено",
620
- providerDrag: "Перетащите для порядка",
644
+ providerDrag: "Приоритет перевода (перетащите для порядка)",
621
645
  providerCancel: "Отмена",
622
646
  providerSave: "Сохранить",
623
647
  providerSaveError: "Не удалось сохранить",
@@ -1974,13 +1998,22 @@ window.__ModuleLoader__.load({
1974
1998
  // URL or a key per provider.
1975
1999
  var dshAll = Object.keys(provs).filter(function (id) { return provs[id] && provs[id].source === "dsh"; });
1976
2000
  var dshAvailable = dshAll.filter(function (id) { return chain.indexOf(id) < 0; });
1977
- var openDshImport = function () {
2001
+ // The host already merges the harness's providers into the config it serves,
2002
+ // so they show up in the "available providers" list without being asked for.
2003
+ // What the button adds is the RE-READ: editing ~/.dsh/settings.yaml (or
2004
+ // fixing the home the host looks at) otherwise needs a web restart.
2005
+ var rescanDsh = function () {
2006
+ setMsg(t.dshRescanning);
1978
2007
  fetch("/_xlate/dsh-scan").then(function (r) { return r.json(); })
1979
2008
  .then(function (c) {
1980
- if (c && c.providers) { cfgRef.current = c; setCfg(c); }
1981
- setAdding(false);
1982
- setEditingId(null);
1983
- setDshMenu(true);
2009
+ if (!c || !c.providers) throw new Error("bad scan response");
2010
+ cfgRef.current = c;
2011
+ setCfg(c);
2012
+ var d = c.dshScan || {};
2013
+ var found = (d.providers || []).length;
2014
+ setMsg(found
2015
+ ? t.dshRescanDone + " · " + found
2016
+ : t.dshImportEmpty + (d.home ? " · " + d.home + "/settings.yaml" : ""));
1984
2017
  })
1985
2018
  .catch(function (e) { setMsg(t.connFail + ": " + (e && e.message ? e.message : e)); });
1986
2019
  };
@@ -1989,7 +2022,6 @@ window.__ModuleLoader__.load({
1989
2022
  dshAvailable.forEach(function (id) { if (next.indexOf(id) < 0) next.push(id); });
1990
2023
  applyLocal({ chain: next });
1991
2024
  saveCfg({ chain: next });
1992
- setDshMenu(false);
1993
2025
  };
1994
2026
 
1995
2027
  // Local-model menu: opened by the chip on the endpoint's own row and
@@ -2127,17 +2159,15 @@ window.__ModuleLoader__.load({
2127
2159
  })),
2128
2160
  createElement("label", { className: "xl-form-row" },
2129
2161
  createElement("span", { className: "xl-form-label" }, t.providerApiKey),
2130
- // The env name has no row of its own (it arrives from a preset or a
2131
- // DSH row), so show it here: without the badge the value is invisible
2132
- // and typing a key would look like it had no effect.
2133
- formApiKeyEnv.trim() ? createElement("span", { className: "xl-badge", title: t.providerApiKey }, "env:" + formApiKeyEnv.trim()) : null,
2162
+ // No env badge here: the apiKeyEnv a preset fills in is an
2163
+ // implementation detail of that endpoint, and the saved row shows an
2164
+ // env:NAME badge anyway. Typing a key clears the env name (below), so
2165
+ // the key the user can see is always the one that wins.
2134
2166
  createElement("input", {
2135
2167
  className: "xl-cfg", placeholder: t.providerApiKey, value: formApiKey,
2136
2168
  onChange: function (e) {
2137
2169
  var v = e.target.value;
2138
2170
  setFormApiKey(v);
2139
- // Typing a key means "use this key": drop the env name, or the
2140
- // host would keep preferring the environment value.
2141
2171
  if (v.trim() !== "") setFormApiKeyEnv("");
2142
2172
  }
2143
2173
  })),
@@ -2154,10 +2184,23 @@ window.__ModuleLoader__.load({
2154
2184
  var unused = Object.keys(provs).filter(function (id) {
2155
2185
  return chain.indexOf(id) < 0;
2156
2186
  });
2187
+ var unusedDsh = unused.filter(function (id) { return provs[id] && provs[id].source === "dsh"; });
2157
2188
  var unusedArea = null;
2158
2189
  if (unused.length > 0) {
2190
+ // The heading names what the rows are — in practice the DSH entries the
2191
+ // host discovered; the generic wording stays for a custom provider that
2192
+ // was saved without a chain reference.
2193
+ var allDsh = unusedDsh.length === unused.length;
2159
2194
  unusedArea = createElement("div", { className: "xl-unused", style: { marginTop: "8px" } },
2160
- createElement("div", { className: "xl-note", style: { fontWeight: 600 } }, t.providerUnused),
2195
+ // The heading row carries the bulk action: the rows are right here, so
2196
+ // this is the one place that lists them (the old separate "inherit"
2197
+ // menu listed the same providers a second time).
2198
+ createElement("div", { style: { display: "flex", alignItems: "center", gap: "6px" } },
2199
+ createElement("div", { className: "xl-note", style: { fontWeight: 600, flex: "1 1 auto" } },
2200
+ allDsh ? t.providerDshList : t.providerUnused),
2201
+ unusedDsh.length > 1 ? createElement("button", {
2202
+ className: "xl-btn", type: "button", onClick: addAllDsh
2203
+ }, t.dshImportAll) : null),
2161
2204
  unused.map(function (id) {
2162
2205
  var up = provs[id] || {};
2163
2206
  var isDsh = up.source === "dsh";
@@ -2165,7 +2208,13 @@ window.__ModuleLoader__.load({
2165
2208
  createElement("span", { className: "xl-provider-type" }, providerLabel(id, up, t)),
2166
2209
  createElement("span", { className: "xl-provider-name" }, id),
2167
2210
  isDsh ? createElement("span", { className: "xl-badge" }, t.providerDsh) : null,
2168
- createElement("button", { className: "xl-btn", type: "button", onClick: function () { actions.addToChain(id); } }, "+ " + t.providerAddToChain));
2211
+ // A bare "+" with the wording in the tooltip: one narrow control
2212
+ // at the end of the row, same as the ↑↓/测试 cluster above.
2213
+ createElement("button", {
2214
+ className: "xl-btn xl-add-btn", type: "button",
2215
+ title: t.providerAddToChain, "aria-label": t.providerAddToChain,
2216
+ onClick: function () { actions.addToChain(id); }
2217
+ }, "+"));
2169
2218
  }));
2170
2219
  }
2171
2220
 
@@ -2177,27 +2226,8 @@ window.__ModuleLoader__.load({
2177
2226
  : createElement("button", { className: "xl-btn", type: "button", onClick: startAdd, style: { marginTop: "4px" } },
2178
2227
  "+ " + t.providerAdd),
2179
2228
  adding || editingId ? null
2180
- : createElement("button", { className: "xl-btn", type: "button", onClick: openDshImport, style: { marginTop: "4px", marginLeft: "6px" } },
2181
- "+ " + t.dshImport + (dshAvailable.length ? " · " + dshAvailable.length : "")),
2182
- dshMenu ? createElement("div", { className: "xl-dsh-menu" },
2183
- dshAvailable.length === 0
2184
- ? createElement("div", { className: "xl-note" }, t.dshImportEmpty)
2185
- : createElement("div", null,
2186
- dshAvailable.length > 1 ? createElement("button", {
2187
- className: "xl-btn", type: "button", style: { marginBottom: "4px" }, onClick: addAllDsh
2188
- }, t.dshImportAll) : null,
2189
- dshAvailable.map(function (id) {
2190
- var dp = provs[id] || {};
2191
- return createElement("div", { key: id, className: "xl-provider-row" },
2192
- createElement("span", { className: "xl-badge" }, t.providerDsh),
2193
- createElement("span", { className: "xl-provider-name" }, id),
2194
- createElement("span", { className: "xl-provider-type" }, providerLabel(id, dp, t)),
2195
- dp.model ? createElement("span", { className: "xl-provider-type" }, dp.model) : null,
2196
- createElement("button", {
2197
- className: "xl-btn", type: "button",
2198
- onClick: function () { actions.addToChain(id); }
2199
- }, "+ " + t.providerAddToChain));
2200
- }))) : null,
2229
+ : createElement("button", { className: "xl-btn", type: "button", onClick: rescanDsh, style: { marginTop: "4px", marginLeft: "6px" } },
2230
+ "↻ " + t.dshImport),
2201
2231
  unusedArea,
2202
2232
  msg ? createElement("div", { className: "xl-note", style: { marginTop: "6px" } }, msg) : null);
2203
2233
  }
@@ -2219,6 +2249,23 @@ window.__ModuleLoader__.load({
2219
2249
  // that node, and taking it out of the tree behind React's back means React
2220
2250
  // can later patch or remove a detached node (NotFoundError on removeChild).
2221
2251
  // A sibling in the same slot looks identical and leaves React's tree intact.
2252
+ function shellNavIcon(btn) {
2253
+ // The SHELL's svg, never ours: once the holder is in place it comes first
2254
+ // in document order, so a plain querySelector('svg') returns the book — and
2255
+ // the next pass then hides the plugin's own glyph. That is exactly how the
2256
+ // nav icon disappeared after the first pass (fixed in 1.2.1).
2257
+ var kids = btn.children || [];
2258
+ for (var i = 0; i < kids.length; i++) {
2259
+ var tag = (kids[i].tagName || "").toLowerCase();
2260
+ if (tag === "svg" && !kids[i].hasAttribute("data-xl-nav-glyph-svg")) return kids[i];
2261
+ }
2262
+ var nested = btn.querySelectorAll ? btn.querySelectorAll("svg") : [];
2263
+ for (var j = 0; j < nested.length; j++) {
2264
+ if (!nested[j].hasAttribute("data-xl-nav-glyph-svg")) return nested[j];
2265
+ }
2266
+ return null;
2267
+ }
2268
+
2222
2269
  function decorateSettingsGlyph() {
2223
2270
  if (typeof document === "undefined") return;
2224
2271
  var st = UI_TEXT[store.lang] || UI_TEXT.en;
@@ -2231,7 +2278,7 @@ window.__ModuleLoader__.load({
2231
2278
  var btn = buttons[j];
2232
2279
  var label = btn.lastElementChild;
2233
2280
  if (!label || (label.textContent || "").trim() !== title) continue;
2234
- var svg = btn.querySelector("svg");
2281
+ var svg = shellNavIcon(btn);
2235
2282
  if (!svg || !svg.parentNode) continue;
2236
2283
  if (!btn.querySelector('[data-xl-nav-glyph="1"]')) {
2237
2284
  var holder = document.createElement("span");
@@ -2242,6 +2289,8 @@ window.__ModuleLoader__.load({
2242
2289
  if (svgClass) holder.setAttribute("class", svgClass);
2243
2290
  holder.style.display = "inline-flex";
2244
2291
  holder.innerHTML = NAV_GLYPH_SVG;
2292
+ var inner = holder.querySelector ? holder.querySelector("svg") : null;
2293
+ if (inner) inner.setAttribute("data-xl-nav-glyph-svg", "1");
2245
2294
  svg.parentNode.insertBefore(holder, svg);
2246
2295
  }
2247
2296
  // Self-healing: the shell may re-render its icon, and the decoration
package/lib/index.js CHANGED
@@ -22,13 +22,40 @@ export const inject = ['webServer', 'subprocess', 'timer']
22
22
  const CONFIG_PATH = fileURLToPath(new URL('./config.json', import.meta.url))
23
23
  const PACKAGE_PATH = fileURLToPath(new URL('../package.json', import.meta.url))
24
24
 
25
- // DSH harness home directory. Used to discover service providers from the
26
- // user's configured llm-pi-ai providers. Falls back to platform defaults
27
- // when the DSH_HOME env var is not set.
28
- const DSH_HOME = process.env.DSH_HOME ||
29
- (process.platform === 'darwin'
30
- ? (process.env.HOME + '/Library/Application Support/dsh-desktop/harness')
31
- : ((process.env.APPDATA || '') + '\\dsh-desktop\\harness'))
25
+ // Where the harness keeps its settings.yaml and .credentials.yaml.
26
+ //
27
+ // This mirrors DSH's own resolution — `$DSH_HOME`, then `~/.dsh` (see
28
+ // @deepseek-ai/dsh-home-paths: an explicit path, then DSH_HOME, then
29
+ // homedir()/.dsh, with a blank DSH_HOME treated as unset) — and only then the
30
+ // desktop-app layouts. Picking one guess was the bug: an install whose home is
31
+ // ~/.dsh fell through to %APPDATA%\dsh-desktop\harness, which does not exist
32
+ // there, so discovery silently reported "no provider configured" while the
33
+ // settings file sat right there. The candidates are resolved per call and the
34
+ // first one that actually holds a settings.yaml wins (see resolveHarnessHome).
35
+ function harnessHomeCandidates() {
36
+ const out = []
37
+ const env = process.env.DSH_HOME
38
+ if (typeof env === 'string' && env.trim()) out.push(env.trim())
39
+ const home = process.env.HOME || process.env.USERPROFILE || ''
40
+ if (home) {
41
+ out.push(home.replace(/[\\/]+$/, '') + '/.dsh')
42
+ if (process.platform === 'darwin') out.push(home.replace(/[\\/]+$/, '') + '/Library/Application Support/dsh-desktop/harness')
43
+ }
44
+ if (process.env.APPDATA) out.push(process.env.APPDATA.replace(/[\\/]+$/, '') + '\\dsh-desktop\\harness')
45
+ return out
46
+ }
47
+
48
+ // The first candidate holding a settings.yaml, else the first candidate at all
49
+ // (so the diagnostic can say which one was looked at).
50
+ function pickHarnessHome(candidates, exists) {
51
+ for (const dir of candidates) {
52
+ try { if (exists(dir + '/settings.yaml')) return { home: dir, settingsFound: true } } catch (e) { /* keep looking */ }
53
+ }
54
+ return { home: candidates[0] || '', settingsFound: false }
55
+ }
56
+
57
+ // Diagnostic for the settings panel: which home was used and what was read there.
58
+ let dshScanDiag = { home: null, settingsFound: false, credsFound: false, providers: [] }
32
59
 
33
60
  // Defaults: local model (Ollama) enabled from the start — the chain is
34
61
  // fail-open, so if Ollama is down it falls back to google/bing. `chain` is the
@@ -744,6 +771,38 @@ function extractDshProviders(txt) {
744
771
  return providers
745
772
  }
746
773
 
774
+ // The harness ALSO has one built-in provider route that never shows up in
775
+ // llm-pi-ai.providers: `deepseek-official`, registered by llm-deepseek and
776
+ // configured through `agent-default-model` (provider + model) plus a credential
777
+ // ref. Its transport is OpenAI-compatible — llm-deepseek itself posts to
778
+ // `${baseURL}/chat/completions` with PUBLIC_BASE_URL = https://api.deepseek.com
779
+ // and the DEEPSEEK_API_KEY credential — so this plugin can drive it with the
780
+ // same adapter, which is what lets "inherit from DSH" offer DeepSeek's own API
781
+ // and not just the custom endpoints someone declared by hand.
782
+ const BUILTIN_HARNESS_ROUTES = {
783
+ 'deepseek-official': { type: 'openai', baseURL: 'https://api.deepseek.com', apiKeyEnv: 'DEEPSEEK_API_KEY' },
784
+ }
785
+
786
+ // Returns { id, entry } for the harness's default model route when the plugin
787
+ // knows how to speak to it, else null. `refs` is the credential map, used only to
788
+ // confirm the key exists somewhere.
789
+ function extractHarnessDefaultRoute(txt, refs) {
790
+ const block = txt.match(/^agent-default-model:[^\n]*\n((?:[ \t]+[^\n]*\n?)*)/m)
791
+ if (!block) return null
792
+ const provider = (block[1].match(/^[ \t]+provider:[ \t]*(\S+)/m) || [])[1]
793
+ const model = (block[1].match(/^[ \t]+model:[ \t]*(\S+)/m) || [])[1]
794
+ if (!provider || !model) return null
795
+ const route = BUILTIN_HARNESS_ROUTES[provider]
796
+ if (!route) return null
797
+ const entry = { type: route.type, baseURL: route.baseURL, model: model }
798
+ // The ref is the route's documented credential; carry it even when the user
799
+ // keeps the key in the environment instead of .credentials.yaml, because the
800
+ // host resolves apiKeyEnv from both.
801
+ if (route.apiKeyEnv) entry.apiKeyEnv = route.apiKeyEnv
802
+ void refs
803
+ return { id: provider, entry: entry }
804
+ }
805
+
747
806
  // (2026-09-01) Parse .credentials.yaml refs. Returns { envName: value }.
748
807
  function extractCredRefs(txt) {
749
808
  const refs = {}
@@ -777,12 +836,17 @@ let dshScanOk = false
777
836
  async function discoverDshProviders() {
778
837
  const fs = await import('node:fs')
779
838
  const out = {}
839
+ const candidates = harnessHomeCandidates()
840
+ const picked = pickHarnessHome(candidates, function (p) { return fs.existsSync(p) })
841
+ const home = picked.home
842
+ const diag = { home: home, candidates: candidates, settingsFound: picked.settingsFound, credsFound: false, providers: [], error: null }
780
843
  try {
781
- const settingsPath = DSH_HOME + '/settings.yaml'
782
- const credsPath = DSH_HOME + '/.credentials.yaml'
844
+ const settingsPath = home + '/settings.yaml'
845
+ const credsPath = home + '/.credentials.yaml'
783
846
  let settingsTxt = '', credsTxt = ''
784
847
  try { settingsTxt = fs.readFileSync(settingsPath, 'utf8') } catch (e) {}
785
848
  try { credsTxt = fs.readFileSync(credsPath, 'utf8') } catch (e) {}
849
+ diag.credsFound = credsTxt.length > 0
786
850
  const providers = extractDshProviders(settingsTxt)
787
851
  const refs = extractCredRefs(credsTxt)
788
852
  dshScanOk = /llm-pi-ai:[\s\S]*?\n[ \t]{2}providers:/m.test(settingsTxt)
@@ -792,7 +856,7 @@ async function discoverDshProviders() {
792
856
  for (const k of Object.keys(refs)) credRefsCache.set(k, refs[k])
793
857
  for (const name of Object.keys(providers)) {
794
858
  const p = providers[name]
795
- if (!p.baseURL || !p.model) continue
859
+ if (!p.baseURL || !p.model) { diag.skipped = (diag.skipped || []).concat(name); continue }
796
860
  const entry = {
797
861
  type: 'openai',
798
862
  source: 'dsh',
@@ -802,8 +866,19 @@ async function discoverDshProviders() {
802
866
  }
803
867
  if (p.apiKeyEnv) entry.apiKeyEnv = p.apiKeyEnv
804
868
  out[name] = entry
869
+ diag.providers.push(name)
805
870
  }
806
- } catch (e) { /* silent — discovery must never block the plugin */ }
871
+ // The harness's built-in route (deepseek-official): never in llm-pi-ai.providers,
872
+ // configured by agent-default-model. An id declared by hand wins.
873
+ const def = extractHarnessDefaultRoute(settingsTxt, refs)
874
+ if (def && !out[def.id]) {
875
+ out[def.id] = Object.assign({ source: 'dsh', enabled: false }, def.entry)
876
+ diag.providers.push(def.id)
877
+ }
878
+ } catch (e) {
879
+ diag.error = e instanceof Error ? e.message : String(e)
880
+ }
881
+ dshScanDiag = diag
807
882
  return out
808
883
  }
809
884
 
@@ -1374,8 +1449,12 @@ export function apply(ctx) {
1374
1449
  const cfg = await loadConfig()
1375
1450
  await saveConfig(cfg)
1376
1451
  // The fresh merge is what fills in resolved credentials, so this route
1377
- // is the one that most needs the sanitizer.
1378
- sendJson(res, 200, stripResolvedKeys(cfg))
1452
+ // is the one that most needs the sanitizer. `dshScan` rides along (on the
1453
+ // response copy only, never saved) so the panel can explain an empty scan
1454
+ // instead of just saying "no provider configured".
1455
+ const out = stripResolvedKeys(cfg)
1456
+ out.dshScan = dshScanDiag
1457
+ sendJson(res, 200, out)
1379
1458
  } catch (e) {
1380
1459
  sendJson(res, 500, { error: e instanceof Error ? e.message : String(e) })
1381
1460
  }
@@ -1390,9 +1469,10 @@ export function apply(ctx) {
1390
1469
  // plugin system (which only reads name, inject, apply) ignores.
1391
1470
  export {
1392
1471
  buildChainFromPriority, migrateConfig, normalizeConfigPatch,
1393
- applyFullProviders, extractDshProviders, extractCredRefs,
1472
+ applyFullProviders, extractDshProviders, extractCredRefs, extractHarnessDefaultRoute,
1394
1473
  runChainFor, translateViaChain, removeFromChain,
1395
1474
  needsTranslate, hasProse, chunkText, fnv, deepMerge,
1396
1475
  structuredCloneSafe, ADAPTERS,
1397
1476
  resolveApiKey, stripResolvedKeys, missingApiKey, clearNullFields, handleVersion,
1477
+ harnessHomeCandidates, pickHarnessHome,
1398
1478
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "dsh-think-translate",
3
3
  "description": "Display-layer translation for the DSH web app: thinking chain, task cards, and optional answer text, in 8 target languages (local Ollama / any OpenAI-compatible or Anthropic endpoint / Google / Bing, fail-open)",
4
- "version": "1.2.0",
4
+ "version": "1.2.2",
5
5
  "type": "module",
6
6
  "main": "lib/index.js",
7
7
  "exports": {