@lynn123411/dsh-chat-translate 3.0.0 → 3.1.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
@@ -85,8 +85,7 @@ async function requestTranslateReply(blocks) {
85
85
  original: block,
86
86
  translated: block,
87
87
  ok: false,
88
- cached: false,
89
- channel: "fallback-client"
88
+ cached: false
90
89
  }));
91
90
  if (!Array.isArray(blocks) || blocks.length === 0) return [];
92
91
  try {
@@ -545,7 +544,7 @@ var zh = {
545
544
  title: "\u6B63\u6587\u7FFB\u8BD1",
546
545
  masterTitle: "\u7FFB\u8BD1\u603B\u5F00\u5173",
547
546
  enableTranslation: "\u542F\u7528\u7FFB\u8BD1",
548
- masterDesc: "\u81EA\u52A8\u628A\u52A9\u624B\u56DE\u7B54\u6B63\u6587\u7FFB\u6210\u4E2D\u6587\uFF08\u5982 {example}\uFF09\uFF1A\u6298\u53E0\u5757\u91CC\u7684\u8FC7\u7A0B\u6B63\u6587\u4E0E\u6700\u7EC8\u6C47\u603B\u4E00\u89C6\u540C\u4EC1\uFF0C\u6574\u6BB5\u4E2D\u6587\u7684\u5757\u4E5F\u7167\u9001\uFF0C\u7531\u6A21\u578B\u4E00\u5E76\u6539\u5199\u6210\u81EA\u7136\u4E2D\u6587\u3001\u987A\u6389\u673A\u5668\u8154\u3002Think \u5361\u4E0E\u5DE5\u5177\u8C03\u7528\u884C\u4E0D\u7FFB\uFF0C\u4EE3\u7801\u5757\u539F\u6837\u4FDD\u7559\u3002\u7FFB\u8BD1\u8FC7\u7684\u5757\u5DE6\u7F18\u6709\u4E00\u6761\u7AD6\u7EBF\uFF1A\u663E\u793A\u8BD1\u6587\u65F6\u662F\u7A0D\u7C97\u7684\u84DD\u7EBF\uFF0C\u70B9\u56DE\u539F\u6587\u53D8\u7EC6\u7070\u7EBF\uFF08\u63D0\u793A\u8FD9\u91CC\u5907\u7740\u8BD1\u6587\uFF0C\u518D\u70B9\u5373\u5207\u56DE\uFF09\uFF0C\u6CA1\u8BD1\u6210\u7684\u6CA1\u6709\u7EBF\u2014\u2014\u7EBF\u7684\u989C\u8272\u4E0E\u7C97\u7EC6\u4E00\u773C\u5206\u6E05\u300C\u6709\u6CA1\u6709\u8BD1\u300D\u548C\u300C\u6B63\u5728\u8BFB\u54EA\u4E00\u7248\u300D\u3002\u70B9\u51FB\u5DF2\u7FFB\u8BD1\u7684\u5757\u53EF\u5728\u8BD1\u6587\u4E0E\u539F\u6587\u95F4\u5207\u6362\uFF0C\u6A21\u578B\u8BA4\u4E3A\u539F\u6837\u6700\u597D\u3001\u8FD4\u56DE\u4E00\u6A21\u4E00\u6837\u7684\u6587\u5B57\u65F6\u540C\u6837\u7B97\u7FFB\u8BD1\u8FC7\u3001\u540C\u6837\u53EF\u5207\u3002\u8BD1\u6587\u6309\u539F markdown \u683C\u5F0F\u91CD\u65B0\u6E32\u67D3\uFF0C\u8868\u683C\u3001\u5217\u8868\u3001\u52A0\u7C97\u4E0E\u884C\u5185\u4EE3\u7801\u4FDD\u6301\u7ED3\u6784\u3002\u56DE\u7B54\u8FD8\u5728\u6D41\u5F0F\u8F93\u51FA\u65F6\u4E0D\u7FFB\uFF0C\u843D\u5B9A\u540E\u6309\u9605\u8BFB\u987A\u5E8F\u9010\u6BB5\u51FA\u4E2D\u6587\u3002\u53EA\u4F5C\u7528\u4E8E\u5F53\u524D\u67E5\u770B\u7684\u4F1A\u8BDD\uFF0C\u8BD1\u6587\u4E0D\u5199\u56DE\u4F1A\u8BDD\u4E0A\u4E0B\u6587\u3002\u8F93\u5165\u6846\u4E0B\u65B9\u90A3\u4E00\u884C\u6709\u540C\u6B3E\u5F00\u5173\uFF0C\u4E24\u5904\u72B6\u6001\u540C\u6B65\u3002",
547
+ masterDesc: "\u81EA\u52A8\u628A\u52A9\u624B\u56DE\u7B54\u6B63\u6587\u7FFB\u6210\u4E2D\u6587\uFF08\u5982 {example}\uFF09\uFF1A\u6298\u53E0\u5757\u91CC\u7684\u8FC7\u7A0B\u6B63\u6587\u4E0E\u6700\u7EC8\u6C47\u603B\u4E00\u89C6\u540C\u4EC1\uFF0C\u6574\u6BB5\u4E2D\u6587\u7684\u5757\u4E5F\u7167\u9001\uFF0C\u7531\u6A21\u578B\u4E00\u5E76\u6539\u5199\u6210\u81EA\u7136\u4E2D\u6587\u3001\u987A\u6389\u673A\u5668\u8154\u3002Think \u5361\u4E0E\u5DE5\u5177\u8C03\u7528\u884C\u4E0D\u7FFB\uFF0C\u4EE3\u7801\u5757\u539F\u6837\u4FDD\u7559\u3002\u6B63\u6587\u5757\u7684\u5DE6\u7F18\u4E00\u6761\u7AD6\u7EBF\u62A5\u51FA\u72B6\u6001\uFF1A\u6B63\u5728\u8BFB\u8BD1\u6587\u662F\u84DD\u7EBF\uFF0C\u5907\u7740\u8BD1\u6587\u6B63\u8BFB\u539F\u6587\u662F\u7070\u7EC6\u7EBF\uFF0C\u6CA1\u8BD1\u6210\u662F\u7EA2\u5B9E\u7EBF\uFF08\u9F20\u6807\u60AC\u505C\u62A5\u51FA\u901A\u9053\u7EC6\u8282\u2014\u2014\u8D85\u65F6\u3001\u65AD\u6D41\u6216\u7A7A\u8FD4\u56DE\uFF1B\u8BD1\u6587\u7684\u7ED3\u6784\u6F02\u79FB\u7531\u5BBF\u4E3B\u81EA\u52A8\u4FEE\u56DE\u6216\u91CD\u63B7\uFF0C\u4E0D\u518D\u56E0\u683C\u5F0F\u95EE\u9898\u6302\u7EA2\u7EBF\uFF09\uFF0C\u6B63\u5728\u8BF7\u6C42\u662F\u7070\u8109\u52A8\u7EBF\uFF0C\u6CA1\u9001\u8FC7\u6A21\u578B\u6216\u5F00\u5173\u5173\u95ED\u6CA1\u6709\u7EBF\u3002\u70B9\u51FB\u5DF2\u7FFB\u8BD1\u7684\u5757\u5728\u8BD1\u6587\u4E0E\u539F\u6587\u95F4\u5207\u6362\u2014\u2014\u6A21\u578B\u8BA4\u4E3A\u539F\u6837\u6700\u597D\u3001\u8FD4\u56DE\u4E00\u6A21\u4E00\u6837\u7684\u6587\u5B57\u65F6\u540C\u6837\u7B97\u7FFB\u8BD1\u8FC7\u3001\u540C\u6837\u53EF\u5207\uFF1B\u70B9\u51FB\u6302\u7EA2\u7EBF\u7684\u5757\u5219\u6574\u884C\u91CD\u65B0\u7FFB\u8BD1\uFF0C\u5DF2\u6210\u529F\u7684\u5757\u4FDD\u6301\u663E\u793A\u4E0D\u6253\u6270\u3002\u901A\u9053\u5931\u8D25\u4E0D\u81EA\u52A8\u91CD\u8BD5\uFF0C\u6062\u590D\u540E\u7531\u4F60\u7684\u70B9\u51FB\u6551\u6D3B\u3002\u8BD1\u6587\u6309\u539F markdown \u683C\u5F0F\u91CD\u65B0\u6E32\u67D3\uFF0C\u8868\u683C\u3001\u5217\u8868\u3001\u52A0\u7C97\u4E0E\u884C\u5185\u4EE3\u7801\u4FDD\u6301\u7ED3\u6784\u3002\u56DE\u7B54\u8FD8\u5728\u6D41\u5F0F\u8F93\u51FA\u65F6\u4E0D\u7FFB\uFF0C\u843D\u5B9A\u540E\u6309\u9605\u8BFB\u987A\u5E8F\u9010\u6BB5\u51FA\u4E2D\u6587\u3002\u53EA\u4F5C\u7528\u4E8E\u5F53\u524D\u67E5\u770B\u7684\u4F1A\u8BDD\uFF0C\u8BD1\u6587\u4E0D\u5199\u56DE\u4F1A\u8BDD\u4E0A\u4E0B\u6587\u3002\u8F93\u5165\u6846\u4E0B\u65B9\u90A3\u4E00\u884C\u6709\u540C\u6B3E\u5F00\u5173\uFF0C\u4E24\u5904\u72B6\u6001\u540C\u6B65\u3002",
549
548
  aiTitle: "AI \u7FFB\u8BD1\uFF08OpenAI \u517C\u5BB9\u534F\u8BAE\uFF09",
550
549
  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",
551
550
  badgeConfigured: "\u5DF2\u914D\u7F6E",
@@ -570,17 +569,19 @@ var zh = {
570
569
  testOk: "\u8FDE\u63A5\u6B63\u5E38\uFF0C\u5EF6\u8FDF {latency}ms",
571
570
  testFail: "\u5931\u8D25\uFF1A{error}",
572
571
  behaviorTitle: "\u7FFB\u8BD1\u8303\u56F4",
573
- behaviorRule: "\u52A9\u624B\u56DE\u7B54\u6B63\u6587\u4E00\u5F8B\u7FFB\u8BD1\uFF0C\u5305\u62EC\u6700\u7EC8\u6C47\u603B\u4E0E\u6574\u6BB5\u4E2D\u6587\u7684\u5757\uFF1A\u4E2D\u6587\u5757\u540C\u6837\u4EA4\u7ED9\u6A21\u578B\u6539\u5199\u6210\u81EA\u7136\u4E2D\u6587\uFF0C\u610F\u4E49\u3001\u672F\u8BED\u4E0E\u6570\u5B57\u4E0D\u53D8\u3002\u601D\u8003\u94FE\u6B63\u6587\u3001\u5DE5\u5177\u8C03\u7528\u6807\u9898\u4E0E\u6298\u53E0\u6458\u8981\u4E0D\u7FFB\u8BD1\uFF0C\u4EE3\u7801\u5757\u53CA\u5176\u5185\u90E8\u539F\u6837\u4FDD\u7559\u3002",
572
+ behaviorRule: "\u52A9\u624B\u56DE\u7B54\u6B63\u6587\u4E00\u5F8B\u7FFB\u8BD1\uFF0C\u5305\u62EC\u6700\u7EC8\u6C47\u603B\u4E0E\u6574\u6BB5\u4E2D\u6587\u7684\u5757\uFF1A\u4E2D\u6587\u5757\u540C\u6837\u4EA4\u7ED9\u6A21\u578B\u6539\u5199\u6210\u81EA\u7136\u4E2D\u6587\uFF0C\u610F\u4E49\u3001\u672F\u8BED\u4E0E\u6570\u5B57\u4E0D\u53D8\uFF1B\u7A7A\u767D\u5757\u4E0D\u9001\u8BD1\u3001\u4E0D\u6302\u7EBF\u3002\u601D\u8003\u94FE\u6B63\u6587\u3001\u5DE5\u5177\u8C03\u7528\u6807\u9898\u4E0E\u6298\u53E0\u6458\u8981\u4E0D\u7FFB\u8BD1\uFF0C\u4EE3\u7801\u5757\u53CA\u5176\u5185\u90E8\u539F\u6837\u4FDD\u7559\u3002",
574
573
  dockLabel: "\u8BD1",
575
574
  dockOn: "\u6B63\u6587\u7FFB\u8BD1\u5DF2\u5F00\u542F\uFF0C\u70B9\u51FB\u5173\u95ED",
576
- dockOff: "\u6B63\u6587\u7FFB\u8BD1\u5DF2\u5173\u95ED\uFF0C\u70B9\u51FB\u5F00\u542F"
575
+ dockOff: "\u6B63\u6587\u7FFB\u8BD1\u5DF2\u5173\u95ED\uFF0C\u70B9\u51FB\u5F00\u542F",
576
+ failTitle: "\u7FFB\u8BD1\u5931\u8D25",
577
+ retryAria: "\u7FFB\u8BD1\u5931\u8D25\uFF0C\u6309 Enter \u91CD\u8BD5"
577
578
  };
578
579
  var en = {
579
580
  pageNav: "Reading",
580
581
  title: "Reply translate",
581
582
  masterTitle: "Translation master switch",
582
583
  enableTranslation: "Enable translation",
583
- masterDesc: "Translates assistant reply prose (e.g. {example}) into Chinese: text inside disclosure blocks and the final summary are treated alike, and blocks written entirely in Chinese go through the model too, which rewrites them into natural Chinese and smooths out machine-flavored phrasing. Think cards and tool-call rows are never translated, and code blocks stay untouched. A translated block carries a left edge line: a slightly thicker blue one while its translation shows, a thin grey one while you read the original (a translation is waiting one click away), and none at all when nothing translated \u2014 colour and weight together tell at a glance both what exists and what you are reading. Click any translated block to switch between translation and original; even when the model decides the original wording is already best and returns it unchanged, it still counts as translated and stays switchable. Translations are re-rendered as Markdown, so tables, lists, bold runs and inline code keep their structure. A reply that is still streaming is left alone until it settles, then prose turns to Chinese paragraph by paragraph in reading order. Applies to the session you are viewing only; translations never enter the conversation context. The same switch sits on the row below the composer, sharing its state.",
584
+ masterDesc: "Automatically translates assistant reply prose (e.g. {example}) into Chinese: text inside disclosure blocks and the final summary are treated alike, and blocks written entirely in Chinese go through the model too, which rewrites them into natural Chinese and smooths out machine-flavored phrasing. Think cards and tool-call rows are never translated, and code blocks stay untouched. Each prose block reports its state with a left-edge line: blue while its translation reads, thin grey while the original reads with a translation waiting one click away, solid red when translation failed (hover the block to read the channel detail \u2014 a timeout, a broken stream or an empty return; the answer's Markdown shape drift is repaired or re-rolled by the host, so formatting no longer draws a red line), a grey pulse while a request is in flight, and nothing at all when the block was never sent or the switch is off. Click a translated block to switch between translation and original \u2014 even when the model returns the wording unchanged it counts as translated and stays switchable; click a red-lined block to re-run the whole row, with already-successful blocks kept on screen undisturbed. Channel failures are never retried automatically: once the channel recovers, your click revives the row. Translations are re-rendered as Markdown, so tables, lists, bold runs and inline code keep their structure. A reply that is still streaming is left alone until it settles, then prose turns to Chinese paragraph by paragraph in reading order. Applies to the session you are viewing only; translations never enter the conversation context. The same switch sits on the row below the composer, sharing its state.",
584
585
  aiTitle: "AI translation (OpenAI-compatible)",
585
586
  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.",
586
587
  badgeConfigured: "Configured",
@@ -605,11 +606,29 @@ var en = {
605
606
  testOk: "Connected, {latency}ms latency",
606
607
  testFail: "Failed: {error}",
607
608
  behaviorTitle: "What gets translated",
608
- behaviorRule: "All assistant reply prose, the final summary and Chinese-only blocks included: Chinese blocks are also handed to the model, which rewrites them into natural Chinese while keeping meaning, terms and numbers unchanged. Think-chain text, tool-call titles and folded summaries are never translated; code blocks and their contents stay untouched.",
609
+ behaviorRule: "All assistant reply prose, the final summary and Chinese-only blocks included: Chinese blocks are also handed to the model, which rewrites them into natural Chinese while keeping meaning, terms and numbers unchanged; blank blocks are never sent and never marked. Think-chain text, tool-call titles and folded summaries are never translated; code blocks and their contents stay untouched.",
609
610
  dockLabel: "Translate",
610
611
  dockOn: "Translation is on; click to turn it off",
611
- dockOff: "Translation is off; click to turn it on"
612
+ dockOff: "Translation is off; click to turn it on",
613
+ failTitle: "Translation failed",
614
+ retryAria: "Translation failed \u2014 press Enter to retry"
612
615
  };
616
+ var rowT = (key) => zh[key] ?? key;
617
+ function bindRowCopy(locale) {
618
+ if (locale && typeof locale.bind === "function") {
619
+ const bound = locale.bind(NS);
620
+ if (typeof bound === "function") rowT = bound;
621
+ }
622
+ }
623
+ function rowCopy() {
624
+ return {
625
+ retryAria: rowT("retryAria"),
626
+ failTitle: (detail) => {
627
+ const label = rowT("failTitle");
628
+ return detail === void 0 || detail === "" || detail === null ? label : `${label} \u2014 ${detail}`;
629
+ }
630
+ };
631
+ }
613
632
 
614
633
  // src/client/settings/ui.tsx
615
634
  var import_jsx_runtime = require("react/jsx-runtime");
@@ -906,6 +925,185 @@ var React = __toESM(require("react"), 1);
906
925
  var import_react4 = require("react");
907
926
  var import_dsh_client_ui_primitives3 = require("@deepseek-ai/dsh-client-ui-primitives");
908
927
 
928
+ // src/client/chat/row-plan.ts
929
+ function isBlankText(value) {
930
+ return (value ?? "").trim() === "";
931
+ }
932
+ function planAssistantRow(input) {
933
+ const { blocks, streaming, interrupted, groupPart, canTranslate, outcomes, rowStatus, originalKeys } = input;
934
+ const hasVisible = streaming || interrupted || blocks.some((block) => block.kind !== "tool-call");
935
+ if (!hasVisible) return { entries: null, texts: [] };
936
+ const texts = [];
937
+ if (groupPart !== "reasoning") {
938
+ for (const block of blocks) {
939
+ if (block.kind === "text" && !isBlankText(block.text)) texts.push(block.text ?? "");
940
+ }
941
+ }
942
+ const entries = [];
943
+ let textIndex = 0;
944
+ let imageRun = null;
945
+ const flushImages = () => {
946
+ if (imageRun !== null) {
947
+ entries.push({ type: "images", key: entries.length, attachments: imageRun });
948
+ imageRun = null;
949
+ }
950
+ };
951
+ for (let i = 0; i < blocks.length; i++) {
952
+ const block = blocks[i];
953
+ if (block === void 0) continue;
954
+ if (groupPart === "reasoning" && block.kind !== "reasoning") continue;
955
+ if (groupPart === "response" && block.kind === "reasoning") continue;
956
+ switch (block.kind) {
957
+ case "text": {
958
+ flushImages();
959
+ const blank = isBlankText(block.text);
960
+ const index = textIndex;
961
+ if (!blank) textIndex += 1;
962
+ const key = entries.length;
963
+ const attemptable = !blank && canTranslate && !streaming;
964
+ const outcome = attemptable && outcomes !== void 0 ? outcomes[index] ?? null : null;
965
+ let mark = null;
966
+ let fail = null;
967
+ if (outcome !== null && outcome.ok) {
968
+ mark = originalKeys?.has(key) ? "original-view" : "translated";
969
+ } else if (outcome !== null) {
970
+ mark = "failed";
971
+ fail = outcome.detail ?? null;
972
+ } else if (attemptable && rowStatus === "pending") {
973
+ mark = "inflight";
974
+ }
975
+ entries.push({
976
+ type: "prose",
977
+ key,
978
+ text: block.text ?? "",
979
+ translated: outcome !== null && outcome.ok ? outcome.translated : null,
980
+ mark,
981
+ fail
982
+ });
983
+ break;
984
+ }
985
+ case "reasoning":
986
+ flushImages();
987
+ entries.push({ type: "reasoning", key: entries.length, blockIndex: i, text: block.text ?? "" });
988
+ break;
989
+ case "image":
990
+ if (imageRun === null) imageRun = [];
991
+ imageRun.push(block.attachment);
992
+ break;
993
+ case "tool-call":
994
+ flushImages();
995
+ break;
996
+ default:
997
+ flushImages();
998
+ entries.push({ type: "unknown", key: entries.length, block: block.block ?? null });
999
+ }
1000
+ }
1001
+ flushImages();
1002
+ const showStopped = interrupted && (groupPart === void 0 || groupPart === "response" || !blocks.some((block) => block.kind !== "reasoning" && block.kind !== "tool-call"));
1003
+ if (showStopped) entries.push({ type: "stopped", key: entries.length });
1004
+ return { entries, texts };
1005
+ }
1006
+
1007
+ // src/client/chat/click-guard.ts
1008
+ function isBareBlockClick(event, view) {
1009
+ const target = event.target;
1010
+ const interactive = typeof target?.closest === "function" ? target.closest('a,button,[role="button"]') : null;
1011
+ if (interactive !== null && interactive !== void 0 && interactive !== event.currentTarget) return false;
1012
+ const selection = typeof view?.getSelection === "function" ? view.getSelection() : null;
1013
+ if (selection !== null && selection !== void 0) {
1014
+ if (selection.isCollapsed === false && String(selection.toString?.() ?? "") !== "") return false;
1015
+ }
1016
+ return true;
1017
+ }
1018
+
1019
+ // src/client/chat/styles.ts
1020
+ var STYLE_TAG_ID = "@lynn123411/dsh-chat-translate/chat-assistant.css";
1021
+ var ASSISTANT_CSS = [
1022
+ // ---- 行骨架(宿主 AssistantMarkdown.root/body 等价) ----
1023
+ ".dsh-ct-root{display:flex;flex-direction:column;font-size:var(--dsh-content-font-size,14px);line-height:calc(24px + var(--dsh-content-font-delta,0px));color:var(--dsw-alias-label-primary)}",
1024
+ ".dsh-ct-body{display:flex;flex-direction:column;gap:16px}",
1025
+ ".dsh-ct-body>[data-turn-process-inline][hidden]{margin-bottom:-16px}",
1026
+ // 宽表格溢出内容列(宿主等价规则,类名 md-table-wide 由 MarkdownText 自己产出)
1027
+ ".dsh-ct-body .md-table-wide{--dsh-table-spare:max(0px, calc((100cqw - var(--dsh-chat-content-width)) / 2));--dsh-table-lead:calc(var(--dsh-table-spare) + min(var(--dsh-chat-content-width), 100cqw) - 100%);box-sizing:border-box;width:calc(100% + var(--dsh-table-lead) + var(--dsh-table-spare));max-width:none;margin-left:calc(-1 * var(--dsh-table-lead));padding-left:var(--dsh-table-lead)}",
1028
+ ".dsh-ct-body .md-table-wide>table{z-index:1;position:relative}",
1029
+ // ---- 已停止标记(宿主 .hWmORq_stopped 等价) ----
1030
+ ".dsh-ct-stopped{border-radius:var(--dsw-radius-sm);background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-tertiary);align-self:flex-start;padding:0 6px;font-size:11px;line-height:18px}",
1031
+ // ---- 「已翻译」交互与标记:可点态常驻(含左缘内缩)——蓝粗线=读译文,
1032
+ // 灰细线=读原文备着译文;区分走色相+粗细 ----
1033
+ ".dsh-ct-prose-clickable{padding-left:12px;cursor:pointer}",
1034
+ ".dsh-ct-prose-clickable:focus-visible{outline:1.5px solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary));outline-offset:2px;border-radius:var(--dsw-radius-sm)}",
1035
+ ".dsh-ct-prose-translated{border-left:1px solid color-mix(in srgb, var(--dsw-alias-state-business-primary) 65%, transparent)}",
1036
+ ".dsh-ct-prose-original{border-left:0.5px solid var(--dsw-alias-border-l2)}",
1037
+ // ---- 失败标记:1px error 色红实线(只剩通道伤一种,悬停报细节);
1038
+ // 可点,点=整行手动补跑 ----
1039
+ ".dsh-ct-prose-failed{border-left:1px solid color-mix(in srgb, var(--dsw-alias-state-error-primary) 65%, transparent)}",
1040
+ // ---- 在途:灰 1px 线脉动(与成功态同 padding,转蓝不跳字);
1041
+ // prefers-reduced-motion 降级为静态灰线 ----
1042
+ ".dsh-ct-prose-inflight{padding-left:12px;border-left:1px solid var(--dsw-alias-border-l2);animation:1.4s ease-in-out infinite alternate dsh-ct-prose-inflight-pulse}",
1043
+ "@keyframes dsh-ct-prose-inflight-pulse{from{border-left-color:var(--dsw-alias-border-l2)}to{border-left-color:color-mix(in srgb, var(--dsw-alias-border-l2) 20%, transparent)}}",
1044
+ "@media (prefers-reduced-motion:reduce){.dsh-ct-prose-inflight{animation:none}}",
1045
+ // ---- 失败块的重试指引:悬停/键盘聚焦时在左侧线槽内露出小 ↻(官方图标,
1046
+ // 文案由 Tooltip 挂在该锚点上);静态不扰动阅读 ----
1047
+ ".dsh-ct-prose-retryable{position:relative}",
1048
+ ".dsh-ct-retry{position:absolute;left:2px;top:6px;display:flex;color:var(--dsw-alias-state-error-primary);opacity:0;visibility:hidden}",
1049
+ ".dsh-ct-prose-retryable:hover .dsh-ct-retry,.dsh-ct-prose-retryable:focus-visible .dsh-ct-retry{opacity:1;visibility:visible}",
1050
+ // ---- ReasoningRow(宿主 lcKema_* 等价,前缀换成 dsh-ct-think) ----
1051
+ ".dsh-ct-think-root{flex-direction:column;display:flex}",
1052
+ ".dsh-ct-think-root:not([data-expanded]){contain:size layout;height:calc(24px + var(--dsh-content-font-delta,0px))}",
1053
+ ".dsh-ct-think-row{position:relative;overflow:hidden}",
1054
+ ".dsh-ct-think-root[data-expanded] [data-open] [data-disclosure-row]{z-index:1;background:var(--dsw-alias-bg-base);position:sticky;top:0}",
1055
+ '.dsh-ct-think-root[data-state=running] .dsh-ct-think-row:after{content:"";inset-block:0;background:linear-gradient(90deg, transparent 0%, color-mix(in srgb, var(--dsw-alias-bg-base) 60%, transparent) 55%, transparent 100%);pointer-events:none;width:300px;animation:2.6s ease-out infinite dsh-ct-reasoning-sweep;position:absolute;left:0}',
1056
+ "@keyframes dsh-ct-reasoning-sweep{0%{left:-300px}90%,to{left:100%}}",
1057
+ "@media (prefers-reduced-motion:reduce){.dsh-ct-think-root[data-state=running] .dsh-ct-think-row:after{animation:none}}",
1058
+ ".dsh-ct-think-leading{flex-shrink:0}",
1059
+ ".dsh-ct-think-chevron{color:var(--dsw-alias-label-secondary)}",
1060
+ ".dsh-ct-think-title{font-weight:400}",
1061
+ ".dsh-ct-think-separator{background:var(--dsw-alias-label-caption);border-radius:1px;flex:none;width:2px;height:2px;margin:0 8px}",
1062
+ ".dsh-ct-think-summary{min-width:0;color:var(--dsw-alias-label-tertiary);font-size:var(--dsh-content-font-size-secondary,13px);line-height:calc(20px + var(--dsh-content-font-delta-secondary,0px));white-space:nowrap;flex:auto;overflow:hidden}",
1063
+ ".dsh-ct-think-summary-text{text-overflow:ellipsis;display:block;overflow:hidden}",
1064
+ ".dsh-ct-think-summary[data-streaming]{mask-image:linear-gradient(90deg,currentcolor calc(100% - 48px),transparent)}",
1065
+ ".dsh-ct-think-summary[data-streaming] .dsh-ct-think-summary-text{text-overflow:clip;overflow:visible}",
1066
+ ".dsh-ct-think-root:not([data-preview]) .dsh-ct-think-separator,.dsh-ct-think-root:not([data-preview]) .dsh-ct-think-summary{display:none}",
1067
+ ".dsh-ct-think-body{padding:4px 0 4px calc(22px + var(--dsh-content-font-delta,0px));min-width:0}",
1068
+ // ---- 无障碍:屏幕阅读器专用文本 ----
1069
+ ".dsh-ct-visually-hidden{clip:rect(0 0 0 0);white-space:nowrap;width:1px;height:1px;position:absolute;overflow:hidden}"
1070
+ ].join("");
1071
+ function proseAction(mark) {
1072
+ switch (mark) {
1073
+ case "translated":
1074
+ case "original-view":
1075
+ return "toggle";
1076
+ case "failed":
1077
+ return "retry";
1078
+ default:
1079
+ return null;
1080
+ }
1081
+ }
1082
+ function proseClassNames(mark) {
1083
+ switch (mark) {
1084
+ case "translated":
1085
+ return "dsh-ct-prose-clickable dsh-ct-prose-translated";
1086
+ case "original-view":
1087
+ return "dsh-ct-prose-clickable dsh-ct-prose-original";
1088
+ case "failed":
1089
+ return "dsh-ct-prose-clickable dsh-ct-prose-failed dsh-ct-prose-retryable";
1090
+ case "inflight":
1091
+ return "dsh-ct-prose-inflight";
1092
+ default:
1093
+ return void 0;
1094
+ }
1095
+ }
1096
+ function ensureAssistantStyles() {
1097
+ if (typeof document === "undefined") return;
1098
+ const selector = `style[data-plugin-css=${JSON.stringify(STYLE_TAG_ID)}]`;
1099
+ if (document.querySelector(selector) !== null) return;
1100
+ const tag = document.createElement("style");
1101
+ tag.dataset.plugin = "@lynn123411/dsh-chat-translate";
1102
+ tag.dataset.pluginCss = STYLE_TAG_ID;
1103
+ tag.textContent = ASSISTANT_CSS;
1104
+ document.head.appendChild(tag);
1105
+ }
1106
+
909
1107
  // src/server/pipeline/blocks.ts
910
1108
  var REPLY_MAX_INPUT_TOKENS = 4096;
911
1109
  var CJK_CHAR = /[\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff\u3000-\u303f\uff00-\uffef]/;
@@ -919,7 +1117,6 @@ function estimateTokens(text) {
919
1117
 
920
1118
  // src/client/chat/translate-store.ts
921
1119
  var MAX_TRANSLATED_ROWS = 200;
922
- var MAX_ROW_ATTEMPTS = 3;
923
1120
  var REQUEST_TOKEN_BUDGET = REPLY_MAX_INPUT_TOKENS;
924
1121
  function chunkTexts(texts) {
925
1122
  const batches = [];
@@ -966,32 +1163,33 @@ var ChatTranslateStore = class {
966
1163
  }
967
1164
  /**
968
1165
  * 确保该行按当前文本被翻译。
969
- * @returns 是否发出了新的请求(同代 done/pending 与换代前的登记之外都是 false)。
1166
+ * @param manual - 用户点击失败块发起的补跑:已落定的同代行只有这条路会重发。
1167
+ * @returns 是否发出了新的请求。
970
1168
  */
971
- ensure(rowKey, texts) {
1169
+ ensure(rowKey, texts, manual = false) {
972
1170
  const current = this.rows.get(rowKey);
973
- let attempts = 1;
1171
+ let seed = null;
974
1172
  if (current !== void 0 && sameTexts(current.texts, texts)) {
975
- if (current.status !== "partial") return false;
976
- if (current.attempts >= MAX_ROW_ATTEMPTS) return false;
977
- attempts = current.attempts + 1;
1173
+ if (current.status === "pending") return false;
1174
+ if (!manual) return false;
1175
+ seed = current.outcomes.map((outcome) => outcome !== null && outcome.ok ? { ...outcome } : null);
978
1176
  }
979
1177
  const snapshot = [...texts];
1178
+ const seedOutcomes = seed ?? snapshot.map(() => null);
980
1179
  const next = {
981
1180
  status: "pending",
982
1181
  texts: snapshot,
983
- outcomes: snapshot.map(() => null),
984
- attempts
1182
+ outcomes: seedOutcomes
985
1183
  };
986
1184
  this.rows.delete(rowKey);
987
1185
  this.rows.set(rowKey, next);
988
1186
  this.prune();
989
1187
  this.bump();
990
- if (snapshot.length > 0) void this.run(rowKey, snapshot);
1188
+ if (snapshot.length > 0) void this.run(rowKey, snapshot, seedOutcomes);
991
1189
  return true;
992
1190
  }
993
- async run(rowKey, texts) {
994
- const outcomes = texts.map(() => null);
1191
+ async run(rowKey, texts, initial) {
1192
+ const outcomes = initial.map((o) => o);
995
1193
  let offset = 0;
996
1194
  for (const batch of chunkTexts(texts)) {
997
1195
  let results = [];
@@ -1005,7 +1203,12 @@ var ChatTranslateStore = class {
1005
1203
  if (result !== void 0 && result.ok && result.translated && result.translated.trim()) {
1006
1204
  outcomes[offset + i] = { translated: result.translated, ok: true };
1007
1205
  } else {
1008
- outcomes[offset + i] = { translated: batch[i] ?? texts[offset + i] ?? "", ok: false };
1206
+ const detail = typeof result?.detail === "string" && result.detail !== "" ? result.detail : void 0;
1207
+ outcomes[offset + i] = {
1208
+ translated: batch[i] ?? texts[offset + i] ?? "",
1209
+ ok: false,
1210
+ ...detail === void 0 ? {} : { detail }
1211
+ };
1009
1212
  }
1010
1213
  }
1011
1214
  offset += batch.length;
@@ -1014,19 +1217,16 @@ var ChatTranslateStore = class {
1014
1217
  this.rows.set(rowKey, {
1015
1218
  status: "pending",
1016
1219
  texts: live.texts,
1017
- outcomes: [...outcomes],
1018
- attempts: live.attempts
1220
+ outcomes: [...outcomes]
1019
1221
  });
1020
1222
  this.bump();
1021
1223
  }
1022
1224
  const state = this.rows.get(rowKey);
1023
1225
  if (state?.status === "pending" && sameTexts(state.texts, texts)) {
1024
- const anyFailed = outcomes.some((outcome) => outcome === null || !outcome.ok);
1025
1226
  this.rows.set(rowKey, {
1026
- status: anyFailed ? "partial" : "done",
1227
+ status: "settled",
1027
1228
  texts: state.texts,
1028
- outcomes,
1029
- attempts: state.attempts
1229
+ outcomes: [...outcomes]
1030
1230
  });
1031
1231
  this.bump();
1032
1232
  }
@@ -1061,133 +1261,6 @@ function createTranslateStore(fetcher) {
1061
1261
  }
1062
1262
  var chatTranslate = createTranslateStore(requestTranslateReply);
1063
1263
 
1064
- // src/client/chat/row-plan.ts
1065
- function planAssistantRow(input) {
1066
- const { blocks, streaming, interrupted, groupPart, canTranslate, outcomes } = input;
1067
- const hasVisible = streaming || interrupted || blocks.some((block) => block.kind !== "tool-call");
1068
- if (!hasVisible) return { entries: null, texts: [] };
1069
- const texts = [];
1070
- if (groupPart !== "reasoning") {
1071
- for (const block of blocks) {
1072
- if (block.kind === "text") texts.push(block.text ?? "");
1073
- }
1074
- }
1075
- const entries = [];
1076
- let textIndex = 0;
1077
- let imageRun = null;
1078
- const flushImages = () => {
1079
- if (imageRun !== null) {
1080
- entries.push({ type: "images", key: entries.length, attachments: imageRun });
1081
- imageRun = null;
1082
- }
1083
- };
1084
- for (let i = 0; i < blocks.length; i++) {
1085
- const block = blocks[i];
1086
- if (block === void 0) continue;
1087
- if (groupPart === "reasoning" && block.kind !== "reasoning") continue;
1088
- if (groupPart === "response" && block.kind === "reasoning") continue;
1089
- switch (block.kind) {
1090
- case "text": {
1091
- flushImages();
1092
- const index = textIndex;
1093
- textIndex += 1;
1094
- const outcome = canTranslate && !streaming && outcomes !== void 0 ? outcomes[index] : null;
1095
- entries.push({
1096
- type: "prose",
1097
- key: entries.length,
1098
- text: block.text ?? "",
1099
- translated: outcome !== null && outcome !== void 0 && outcome.ok ? outcome.translated : null
1100
- });
1101
- break;
1102
- }
1103
- case "reasoning":
1104
- flushImages();
1105
- entries.push({ type: "reasoning", key: entries.length, blockIndex: i, text: block.text ?? "" });
1106
- break;
1107
- case "image":
1108
- if (imageRun === null) imageRun = [];
1109
- imageRun.push(block.attachment);
1110
- break;
1111
- case "tool-call":
1112
- flushImages();
1113
- break;
1114
- default:
1115
- flushImages();
1116
- entries.push({ type: "unknown", key: entries.length, block: block.block ?? null });
1117
- }
1118
- }
1119
- flushImages();
1120
- const showStopped = interrupted && (groupPart === void 0 || groupPart === "response" || !blocks.some((block) => block.kind !== "reasoning" && block.kind !== "tool-call"));
1121
- if (showStopped) entries.push({ type: "stopped", key: entries.length });
1122
- return { entries, texts };
1123
- }
1124
-
1125
- // src/client/chat/click-guard.ts
1126
- function isBareBlockClick(event, view) {
1127
- const target = event.target;
1128
- const interactive = typeof target?.closest === "function" ? target.closest('a,button,[role="button"]') : null;
1129
- if (interactive !== null && interactive !== void 0 && interactive !== event.currentTarget) return false;
1130
- const selection = typeof view?.getSelection === "function" ? view.getSelection() : null;
1131
- if (selection !== null && selection !== void 0) {
1132
- if (selection.isCollapsed === false && String(selection.toString?.() ?? "") !== "") return false;
1133
- }
1134
- return true;
1135
- }
1136
-
1137
- // src/client/chat/styles.ts
1138
- var STYLE_TAG_ID = "@lynn123411/dsh-chat-translate/chat-assistant.css";
1139
- var ASSISTANT_CSS = [
1140
- // ---- 行骨架(宿主 AssistantMarkdown.root/body 等价) ----
1141
- ".dsh-ct-root{display:flex;flex-direction:column;font-size:var(--dsh-content-font-size,14px);line-height:calc(24px + var(--dsh-content-font-delta,0px));color:var(--dsw-alias-label-primary)}",
1142
- ".dsh-ct-body{display:flex;flex-direction:column;gap:16px}",
1143
- ".dsh-ct-body>[data-turn-process-inline][hidden]{margin-bottom:-16px}",
1144
- // 宽表格溢出内容列(宿主等价规则,类名 md-table-wide 由 MarkdownText 自己产出)
1145
- ".dsh-ct-body .md-table-wide{--dsh-table-spare:max(0px, calc((100cqw - var(--dsh-chat-content-width)) / 2));--dsh-table-lead:calc(var(--dsh-table-spare) + min(var(--dsh-chat-content-width), 100cqw) - 100%);box-sizing:border-box;width:calc(100% + var(--dsh-table-lead) + var(--dsh-table-spare));max-width:none;margin-left:calc(-1 * var(--dsh-table-lead));padding-left:var(--dsh-table-lead)}",
1146
- ".dsh-ct-body .md-table-wide>table{z-index:1;position:relative}",
1147
- // ---- 已停止标记(宿主 .hWmORq_stopped 等价) ----
1148
- ".dsh-ct-stopped{border-radius:var(--dsw-radius-sm);background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-tertiary);align-self:flex-start;padding:0 6px;font-size:11px;line-height:18px}",
1149
- // ---- 「已翻译」交互与标记:可点态常驻(含左缘内缩);区分走色相+粗细
1150
- // (0.5px 虚线会被抗锯齿糊成实线)——蓝粗线=读译文,灰细线=读原文备着译文 ----
1151
- ".dsh-ct-prose-clickable{padding-left:12px;cursor:pointer}",
1152
- ".dsh-ct-prose-clickable:focus-visible{outline:1.5px solid var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary));outline-offset:2px;border-radius:var(--dsw-radius-sm)}",
1153
- ".dsh-ct-prose-translated{border-left:1px solid color-mix(in srgb, var(--dsw-alias-state-business-primary) 65%, transparent)}",
1154
- ".dsh-ct-prose-original{border-left:0.5px solid var(--dsw-alias-border-l2)}",
1155
- // ---- ReasoningRow(宿主 lcKema_* 等价,前缀换成 dsh-ct-think) ----
1156
- ".dsh-ct-think-root{flex-direction:column;display:flex}",
1157
- ".dsh-ct-think-root:not([data-expanded]){contain:size layout;height:calc(24px + var(--dsh-content-font-delta,0px))}",
1158
- ".dsh-ct-think-row{position:relative;overflow:hidden}",
1159
- ".dsh-ct-think-root[data-expanded] [data-open] [data-disclosure-row]{z-index:1;background:var(--dsw-alias-bg-base);position:sticky;top:0}",
1160
- '.dsh-ct-think-root[data-state=running] .dsh-ct-think-row:after{content:"";inset-block:0;background:linear-gradient(90deg, transparent 0%, color-mix(in srgb, var(--dsw-alias-bg-base) 60%, transparent) 55%, transparent 100%);pointer-events:none;width:300px;animation:2.6s ease-out infinite dsh-ct-reasoning-sweep;position:absolute;left:0}',
1161
- "@keyframes dsh-ct-reasoning-sweep{0%{left:-300px}90%,to{left:100%}}",
1162
- "@media (prefers-reduced-motion:reduce){.dsh-ct-think-root[data-state=running] .dsh-ct-think-row:after{animation:none}}",
1163
- ".dsh-ct-think-leading{flex-shrink:0}",
1164
- ".dsh-ct-think-chevron{color:var(--dsw-alias-label-secondary)}",
1165
- ".dsh-ct-think-title{font-weight:400}",
1166
- ".dsh-ct-think-separator{background:var(--dsw-alias-label-caption);border-radius:1px;flex:none;width:2px;height:2px;margin:0 8px}",
1167
- ".dsh-ct-think-summary{min-width:0;color:var(--dsw-alias-label-tertiary);font-size:var(--dsh-content-font-size-secondary,13px);line-height:calc(20px + var(--dsh-content-font-delta-secondary,0px));white-space:nowrap;flex:auto;overflow:hidden}",
1168
- ".dsh-ct-think-summary-text{text-overflow:ellipsis;display:block;overflow:hidden}",
1169
- ".dsh-ct-think-summary[data-streaming]{mask-image:linear-gradient(90deg,currentcolor calc(100% - 48px),transparent)}",
1170
- ".dsh-ct-think-summary[data-streaming] .dsh-ct-think-summary-text{text-overflow:clip;overflow:visible}",
1171
- ".dsh-ct-think-root:not([data-preview]) .dsh-ct-think-separator,.dsh-ct-think-root:not([data-preview]) .dsh-ct-think-summary{display:none}",
1172
- ".dsh-ct-think-body{padding:4px 0 4px calc(22px + var(--dsh-content-font-delta,0px));min-width:0}",
1173
- // ---- 无障碍:屏幕阅读器专用文本 ----
1174
- ".dsh-ct-visually-hidden{clip:rect(0 0 0 0);white-space:nowrap;width:1px;height:1px;position:absolute;overflow:hidden}"
1175
- ].join("");
1176
- function proseClassNames(processed, showOriginal) {
1177
- if (!processed) return void 0;
1178
- return showOriginal ? "dsh-ct-prose-clickable dsh-ct-prose-original" : "dsh-ct-prose-clickable dsh-ct-prose-translated";
1179
- }
1180
- function ensureAssistantStyles() {
1181
- if (typeof document === "undefined") return;
1182
- const selector = `style[data-plugin-css=${JSON.stringify(STYLE_TAG_ID)}]`;
1183
- if (document.querySelector(selector) !== null) return;
1184
- const tag = document.createElement("style");
1185
- tag.dataset.plugin = "@lynn123411/dsh-chat-translate";
1186
- tag.dataset.pluginCss = STYLE_TAG_ID;
1187
- tag.textContent = ASSISTANT_CSS;
1188
- document.head.appendChild(tag);
1189
- }
1190
-
1191
1264
  // src/client/chat/assistant-step.tsx
1192
1265
  var thinkIcon = () => React.createElement(import_dsh_client_ui_primitives3.IconThinkOutlineRegular, { size: 14 });
1193
1266
  function markdownLabels(t) {
@@ -1352,44 +1425,55 @@ var ReasoningRow = (0, import_react4.memo)(function ReasoningRow2({
1352
1425
  )
1353
1426
  );
1354
1427
  });
1355
- function blockClick(event, onToggle) {
1428
+ function blockClick(event, action) {
1356
1429
  const guardEvent = {
1357
1430
  target: event.target,
1358
1431
  currentTarget: event.currentTarget
1359
1432
  };
1360
- if (isBareBlockClick(guardEvent, typeof window === "undefined" ? void 0 : window)) onToggle();
1433
+ if (isBareBlockClick(guardEvent, typeof window === "undefined" ? void 0 : window)) action();
1361
1434
  }
1362
1435
  function ProseBlock({
1363
1436
  text,
1364
1437
  translated,
1365
- showOriginal,
1438
+ mark,
1439
+ fail,
1366
1440
  onToggle,
1441
+ onRetry,
1367
1442
  streaming,
1368
1443
  labels,
1369
1444
  mentions,
1370
1445
  pathImages
1371
1446
  }) {
1372
- const processed = translated !== null;
1373
- const showTranslation = processed && !showOriginal;
1374
- const source = showTranslation ? translated : text;
1375
- return React.createElement(
1447
+ const action = proseAction(mark);
1448
+ const clickable = action !== null;
1449
+ const retrying = action === "retry";
1450
+ const handler = retrying ? onRetry : onToggle;
1451
+ const source = mark === "translated" ? translated : text;
1452
+ const copy = rowCopy();
1453
+ const block = React.createElement(
1376
1454
  "div",
1377
1455
  {
1378
- // 色相+粗细随显示态:蓝粗线=译文,灰细线=有译文但读原文(仍可点切回),无线=没译。
1379
- className: proseClassNames(processed, showOriginal),
1380
- "data-translated": showTranslation ? "true" : void 0,
1456
+ className: proseClassNames(mark),
1457
+ "data-translated": mark === "translated" ? "true" : void 0,
1381
1458
  // 容器内含链接等交互内容,不套 role=button(非法嵌套);可聚焦 + 键盘
1382
- // Enter/Space 即切换,满足官方「键盘可达」门。
1383
- tabIndex: processed ? 0 : void 0,
1384
- onClick: processed ? (event) => blockClick(event, onToggle) : void 0,
1385
- onKeyDown: processed ? (event) => {
1459
+ // Enter/Space 即切换/重试,满足官方「键盘可达」门。在途块也保持可聚焦
1460
+ // (Enter 空操作)——补跑把红线折成脉动时,键盘焦点不致从行上掉回 body。
1461
+ tabIndex: clickable || mark === "inflight" ? 0 : void 0,
1462
+ onClick: clickable ? (event) => blockClick(event, handler) : void 0,
1463
+ onKeyDown: clickable ? (event) => {
1386
1464
  if (event.target !== event.currentTarget) return;
1387
1465
  if (event.key === "Enter" || event.key === " ") {
1388
1466
  event.preventDefault();
1389
- onToggle();
1467
+ handler();
1390
1468
  }
1391
1469
  } : void 0
1392
1470
  },
1471
+ retrying && React.createElement(
1472
+ "span",
1473
+ { className: "dsh-ct-retry", "aria-hidden": true },
1474
+ React.createElement(import_dsh_client_ui_primitives3.IconRefreshOutlineRegular, { size: 10 })
1475
+ ),
1476
+ retrying && React.createElement("span", { className: "dsh-ct-visually-hidden" }, copy.retryAria),
1393
1477
  React.createElement(import_dsh_client_ui_primitives3.MarkdownText, {
1394
1478
  text: source,
1395
1479
  streaming,
@@ -1398,6 +1482,13 @@ function ProseBlock({
1398
1482
  pathImages
1399
1483
  })
1400
1484
  );
1485
+ if (!retrying) return block;
1486
+ return React.createElement(import_dsh_client_ui_primitives3.Tooltip, {
1487
+ label: () => copy.failTitle(fail),
1488
+ side: "right",
1489
+ portal: true,
1490
+ children: block
1491
+ });
1401
1492
  }
1402
1493
  var AssistantStepView = (0, import_react4.memo)(function AssistantStepView2(props) {
1403
1494
  ensureAssistantStyles();
@@ -1441,7 +1532,9 @@ var AssistantStepView = (0, import_react4.memo)(function AssistantStepView2(prop
1441
1532
  if (streaming || !canTranslate || !inView || plan.entries === null || plan.texts.length === 0) return;
1442
1533
  chatTranslate.ensure(rowKey, plan.texts);
1443
1534
  }, [streaming, canTranslate, inView, rowKey, plan]);
1444
- const rowState = chatTranslate.getState(rowKey);
1535
+ const [originalKeys, setOriginalKeys] = (0, import_react4.useState)(() => /* @__PURE__ */ new Set());
1536
+ const registered = chatTranslate.getState(rowKey);
1537
+ const rowState = registered !== void 0 && sameTexts(registered.texts, plan.texts) ? registered : void 0;
1445
1538
  const displayed = (0, import_react4.useMemo)(
1446
1539
  () => planAssistantRow({
1447
1540
  blocks,
@@ -1449,15 +1542,16 @@ var AssistantStepView = (0, import_react4.memo)(function AssistantStepView2(prop
1449
1542
  interrupted,
1450
1543
  groupPart,
1451
1544
  canTranslate,
1452
- outcomes: rowState?.outcomes
1545
+ outcomes: rowState?.outcomes,
1546
+ rowStatus: rowState?.status,
1547
+ originalKeys
1453
1548
  }),
1454
- [blocks, streaming, interrupted, groupPart, canTranslate, rowState]
1549
+ [blocks, streaming, interrupted, groupPart, canTranslate, rowState, originalKeys]
1455
1550
  );
1456
1551
  const reasoningHidden = turnProcess !== void 0 && turnProcess.foldable && turnProcess.spec.answerStep === data.step && turnProcess.spec.inlineReasoning && !turnProcess.open;
1457
1552
  const revealProcess = (0, import_react4.useCallback)(() => {
1458
1553
  turnProcess?.setOpen(true);
1459
1554
  }, [turnProcess]);
1460
- const [originalKeys, setOriginalKeys] = (0, import_react4.useState)(() => /* @__PURE__ */ new Set());
1461
1555
  const toggleBlock = (0, import_react4.useCallback)((key) => {
1462
1556
  setOriginalKeys((current) => {
1463
1557
  const next = new Set(current);
@@ -1466,6 +1560,9 @@ var AssistantStepView = (0, import_react4.memo)(function AssistantStepView2(prop
1466
1560
  return next;
1467
1561
  });
1468
1562
  }, []);
1563
+ const retryRow = (0, import_react4.useCallback)(() => {
1564
+ chatTranslate.ensure(rowKey, plan.texts, true);
1565
+ }, [rowKey, plan]);
1469
1566
  if (displayed.entries === null) return null;
1470
1567
  const last = blocks.length - 1;
1471
1568
  const rendered = [];
@@ -1477,8 +1574,10 @@ var AssistantStepView = (0, import_react4.memo)(function AssistantStepView2(prop
1477
1574
  key: entry.key,
1478
1575
  text: entry.text,
1479
1576
  translated: entry.translated,
1480
- showOriginal: originalKeys.has(entry.key),
1577
+ mark: entry.mark,
1578
+ fail: entry.fail,
1481
1579
  onToggle: () => toggleBlock(entry.key),
1580
+ onRetry: retryRow,
1482
1581
  streaming,
1483
1582
  labels,
1484
1583
  mentions,
@@ -1648,6 +1747,7 @@ function apply(ctx) {
1648
1747
  }, "dsh-chat-translate: chat presentation source");
1649
1748
  ctx.effect(() => setupSettingsUi(ctx), "dsh-chat-translate: settings section");
1650
1749
  ctx.effect(() => setupComposerToggle(ctx), "dsh-chat-translate: composer dock toggle");
1750
+ bindRowCopy(ctx.locale);
1651
1751
  }
1652
1752
  return module.exports; } });
1653
1753
  //# sourceMappingURL=client.js.map