@lynn123411/dsh-chat-translate 3.0.1 → 3.2.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\u70ED\u533A\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\u5207\u6362\u4E0D\u5728\u6B63\u6587\u4E0A\uFF1A\u8D34\u7740\u90A3\u6761\u7EBF\u7684\u7A84\u5E26\u624D\u662F\u64CD\u4F5C\u533A\uFF0C\u9F20\u6807\u79FB\u5230\u5B83\u4E0A\u9762\u53D8\u624B\u578B\uFF0C\u70B9\u4E00\u4E0B\u5728\u8BD1\u6587\u4E0E\u539F\u6587\u95F4\u5207\u6362\u2014\u2014\u6B63\u6587\u6574\u5757\u4E0D\u518D\u54CD\u5E94\u70B9\u51FB\uFF0C\u9009\u5B57\u3001\u62D6\u9009\u590D\u5236\u90FD\u4E0D\u4F1A\u8BEF\u89E6\uFF1B\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\u6302\u7EA2\u7EBF\u7684\u5757\u53F3\u4FA7\u5E38\u9A7B\u4E00\u4E2A\u5C0F \u21BB\uFF0C\u70B9\u90A3\u6761\u7A84\u5E26\u5373\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\u70B9\u90A3\u6761\u7A84\u5E26\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,22 @@ 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\u70B9\u5DE6\u7F18\u90A3\u6761\u7A84\u5E26\u53EF\u6309 Enter \u91CD\u8BD5",
578
+ toggleToOriginalAria: "\u67E5\u770B\u539F\u6587",
579
+ toggleToTranslatedAria: "\u67E5\u770B\u8BD1\u6587",
580
+ inflightAria: "\u7FFB\u8BD1\u4E2D\uFF0C\u6B64\u5904\u6682\u65E0\u52A8\u4F5C"
577
581
  };
578
582
  var en = {
579
583
  pageNav: "Reading",
580
584
  title: "Reply translate",
581
585
  masterTitle: "Translation master switch",
582
586
  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.",
587
+ 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 slim strip beside the line 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. Switching does not live on the prose: a slim strip hugging that line is the control \u2014 the pointer turns into a hand over it, and one click swaps translation and original, while the prose itself no longer reacts to clicks, so selecting or drag-copying text never triggers a swap by accident. A reply the model returned unchanged still counts as translated and stays switchable. A red-lined block keeps a small \u21BB beside it; clicking the strip re-runs the whole row, with already-successful blocks kept on screen undisturbed. Channel failures are never retried automatically: once the channel recovers, a click on that strip 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
588
  aiTitle: "AI translation (OpenAI-compatible)",
585
589
  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
590
  badgeConfigured: "Configured",
@@ -605,11 +609,35 @@ var en = {
605
609
  testOk: "Connected, {latency}ms latency",
606
610
  testFail: "Failed: {error}",
607
611
  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.",
612
+ 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
613
  dockLabel: "Translate",
610
614
  dockOn: "Translation is on; click to turn it off",
611
- dockOff: "Translation is off; click to turn it on"
615
+ dockOff: "Translation is off; click to turn it on",
616
+ failTitle: "Translation failed",
617
+ retryAria: "Translation failed \u2014 focus the slim strip at the left edge and press Enter to retry",
618
+ toggleToOriginalAria: "Show the original",
619
+ toggleToTranslatedAria: "Show the translation",
620
+ inflightAria: "Translating; Enter does nothing yet"
612
621
  };
622
+ var rowT = (key) => zh[key] ?? key;
623
+ function bindRowCopy(locale) {
624
+ if (locale && typeof locale.bind === "function") {
625
+ const bound = locale.bind(NS);
626
+ if (typeof bound === "function") rowT = bound;
627
+ }
628
+ }
629
+ function rowCopy() {
630
+ return {
631
+ retryAria: rowT("retryAria"),
632
+ toggleToOriginalAria: rowT("toggleToOriginalAria"),
633
+ toggleToTranslatedAria: rowT("toggleToTranslatedAria"),
634
+ inflightAria: rowT("inflightAria"),
635
+ failTitle: (detail) => {
636
+ const label = rowT("failTitle");
637
+ return detail === void 0 || detail === "" || detail === null ? label : `${label} \u2014 ${detail}`;
638
+ }
639
+ };
640
+ }
613
641
 
614
642
  // src/client/settings/ui.tsx
615
643
  var import_jsx_runtime = require("react/jsx-runtime");
@@ -906,6 +934,196 @@ var React = __toESM(require("react"), 1);
906
934
  var import_react4 = require("react");
907
935
  var import_dsh_client_ui_primitives3 = require("@deepseek-ai/dsh-client-ui-primitives");
908
936
 
937
+ // src/client/chat/row-plan.ts
938
+ function isBlankText(value) {
939
+ return (value ?? "").trim() === "";
940
+ }
941
+ function planAssistantRow(input) {
942
+ const { blocks, streaming, interrupted, groupPart, canTranslate, outcomes, rowStatus, originalKeys } = input;
943
+ const hasVisible = streaming || interrupted || blocks.some((block) => block.kind !== "tool-call");
944
+ if (!hasVisible) return { entries: null, texts: [] };
945
+ const texts = [];
946
+ if (groupPart !== "reasoning") {
947
+ for (const block of blocks) {
948
+ if (block.kind === "text" && !isBlankText(block.text)) texts.push(block.text ?? "");
949
+ }
950
+ }
951
+ const entries = [];
952
+ let textIndex = 0;
953
+ let imageRun = null;
954
+ const flushImages = () => {
955
+ if (imageRun !== null) {
956
+ entries.push({ type: "images", key: entries.length, attachments: imageRun });
957
+ imageRun = null;
958
+ }
959
+ };
960
+ for (let i = 0; i < blocks.length; i++) {
961
+ const block = blocks[i];
962
+ if (block === void 0) continue;
963
+ if (groupPart === "reasoning" && block.kind !== "reasoning") continue;
964
+ if (groupPart === "response" && block.kind === "reasoning") continue;
965
+ switch (block.kind) {
966
+ case "text": {
967
+ flushImages();
968
+ const blank = isBlankText(block.text);
969
+ const index = textIndex;
970
+ if (!blank) textIndex += 1;
971
+ const key = entries.length;
972
+ const attemptable = !blank && canTranslate && !streaming;
973
+ const outcome = attemptable && outcomes !== void 0 ? outcomes[index] ?? null : null;
974
+ let mark = null;
975
+ let fail = null;
976
+ if (outcome !== null && outcome.ok) {
977
+ mark = originalKeys?.has(key) ? "original-view" : "translated";
978
+ } else if (outcome !== null) {
979
+ mark = "failed";
980
+ fail = outcome.detail ?? null;
981
+ } else if (attemptable && rowStatus === "pending") {
982
+ mark = "inflight";
983
+ }
984
+ entries.push({
985
+ type: "prose",
986
+ key,
987
+ text: block.text ?? "",
988
+ translated: outcome !== null && outcome.ok ? outcome.translated : null,
989
+ mark,
990
+ fail
991
+ });
992
+ break;
993
+ }
994
+ case "reasoning":
995
+ flushImages();
996
+ entries.push({ type: "reasoning", key: entries.length, blockIndex: i, text: block.text ?? "" });
997
+ break;
998
+ case "image":
999
+ if (imageRun === null) imageRun = [];
1000
+ imageRun.push(block.attachment);
1001
+ break;
1002
+ case "tool-call":
1003
+ flushImages();
1004
+ break;
1005
+ default:
1006
+ flushImages();
1007
+ entries.push({ type: "unknown", key: entries.length, block: block.block ?? null });
1008
+ }
1009
+ }
1010
+ flushImages();
1011
+ const showStopped = interrupted && (groupPart === void 0 || groupPart === "response" || !blocks.some((block) => block.kind !== "reasoning" && block.kind !== "tool-call"));
1012
+ if (showStopped) entries.push({ type: "stopped", key: entries.length });
1013
+ return { entries, texts };
1014
+ }
1015
+
1016
+ // src/client/chat/click-guard.ts
1017
+ function isCleanHotspotClick(event, view) {
1018
+ if (event.target !== event.currentTarget) return false;
1019
+ const selection = typeof view?.getSelection === "function" ? view.getSelection() : null;
1020
+ if (selection !== null && selection !== void 0) {
1021
+ if (selection.isCollapsed === false && String(selection.toString?.() ?? "") !== "") return false;
1022
+ }
1023
+ return true;
1024
+ }
1025
+
1026
+ // src/client/chat/styles.ts
1027
+ var STYLE_TAG_ID = "@lynn123411/dsh-chat-translate/chat-assistant.css";
1028
+ var ASSISTANT_CSS = [
1029
+ // ---- 行骨架(宿主 AssistantMarkdown.root/body 等价) ----
1030
+ ".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)}",
1031
+ ".dsh-ct-body{display:flex;flex-direction:column;gap:16px}",
1032
+ ".dsh-ct-body>[data-turn-process-inline][hidden]{margin-bottom:-16px}",
1033
+ // 宽表格溢出内容列(宿主等价规则,类名 md-table-wide 由 MarkdownText 自己产出)
1034
+ ".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)}",
1035
+ ".dsh-ct-body .md-table-wide>table{z-index:1;position:relative}",
1036
+ // ---- 已停止标记(宿主 .hWmORq_stopped 等价) ----
1037
+ ".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}",
1038
+ // ---- 正文块:可翻译时左缘留出 12px 缩进(缩进与线无关——译文出现前后
1039
+ // 正文位置不变,切换总开关时整列也不横移),并成为热区的定位父级 ----
1040
+ ".dsh-ct-prose{position:relative}",
1041
+ ".dsh-ct-prose-indent{padding-left:12px}",
1042
+ // ---- 线型:粗细与色相只表示状态。蓝粗线=读译文,灰细线=读原文备着译文
1043
+ // (0.5px 中性 hairline),红实线=失败(只剩通道伤一种) ----
1044
+ ".dsh-ct-prose-translated{border-left:1px solid color-mix(in srgb, var(--dsw-alias-state-business-primary) 65%, transparent)}",
1045
+ ".dsh-ct-prose-original{border-left:0.5px solid var(--dsw-alias-border-l2)}",
1046
+ ".dsh-ct-prose-failed{border-left:1px solid color-mix(in srgb, var(--dsw-alias-state-error-primary) 65%, transparent)}",
1047
+ // ---- 在途:灰 1px 线脉动(缩进由 .dsh-ct-prose-indent 统一给,转蓝不跳字);
1048
+ // prefers-reduced-motion 降级为静态灰线 ----
1049
+ ".dsh-ct-prose-inflight{border-left:1px solid var(--dsw-alias-border-l2);animation:1.4s ease-in-out infinite alternate dsh-ct-prose-inflight-pulse}",
1050
+ "@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)}}",
1051
+ "@media (prefers-reduced-motion:reduce){.dsh-ct-prose-inflight{animation:none}}",
1052
+ // ---- 左缘热区:随块高铺满的一条透明窄带。绝对定位的偏移以 padding box 为
1053
+ // 基准、而线画在 border 上,所以线的左缘在 -1px:`left:-5px` 给出线外侧
1054
+ // 4px,`width:17px` 让右缘落在正文第一个字上(12px 缩进),不吃正文。
1055
+ // 切换与补跑只由它承载;正文整块不再是点击目标,选中/复制不再误触。
1056
+ // z-index 压过宽表格的透明出血层(那条规则给 table 挂了 z-index:1);
1057
+ // 出血区是透明 padding,正文格子的起点仍在缩进之外 ----
1058
+ ".dsh-ct-hotspot{position:absolute;left:-5px;top:0;bottom:0;width:17px;margin:0;padding:0;border:0;background:0 0;cursor:pointer;z-index:2;-webkit-appearance:none;appearance:none}",
1059
+ // 在途只剩「已在跑」这一答复:热区保留(键盘焦点不掉),但光标不承诺能点。
1060
+ ".dsh-ct-hotspot[data-idle]{cursor:default}",
1061
+ // 热区没有自己的外形:它是透明的一层。focus-visible 只对键盘聚焦成立(指针
1062
+ // 点出来的焦点不匹配),下面的 :has 规则把线加亮——粗细始终只表示译文/原文。
1063
+ ".dsh-ct-hotspot:focus-visible{outline:none}",
1064
+ ".dsh-ct-prose-translated:has(.dsh-ct-hotspot:focus-visible){border-left-color:var(--dsw-alias-state-business-primary)}",
1065
+ ".dsh-ct-prose-original:has(.dsh-ct-hotspot:focus-visible){border-left-color:var(--dsw-alias-label-caption)}",
1066
+ ".dsh-ct-prose-failed:has(.dsh-ct-hotspot:focus-visible){border-left-color:var(--dsw-alias-state-error-primary)}",
1067
+ // 在途线本就在动;CSS 动画压过普通声明,聚焦的加亮必须同时停掉动画才看得见
1068
+ // (停掉脉动本身就是一次可见的变化,线也从闪烁的灰换成静止的亮灰)。
1069
+ ".dsh-ct-prose-inflight:has(.dsh-ct-hotspot:focus-visible){animation:none;border-left-color:var(--dsw-alias-label-caption)}",
1070
+ // ---- 失败块的补跑指引:小 ↻(官方图标)常驻在红线旁——失败不会被悬停
1071
+ // 发现,救活入口必须一直看得见;纯装饰(pointer-events:none),点击
1072
+ // 归热区,败因由 Tooltip 挂在热区上 ----
1073
+ ".dsh-ct-retry{position:absolute;left:2px;top:6px;display:flex;color:var(--dsw-alias-state-error-primary);pointer-events:none;z-index:3}",
1074
+ // ---- ReasoningRow(宿主 lcKema_* 等价,前缀换成 dsh-ct-think) ----
1075
+ ".dsh-ct-think-root{flex-direction:column;display:flex}",
1076
+ ".dsh-ct-think-root:not([data-expanded]){contain:size layout;height:calc(24px + var(--dsh-content-font-delta,0px))}",
1077
+ ".dsh-ct-think-row{position:relative;overflow:hidden}",
1078
+ ".dsh-ct-think-root[data-expanded] [data-open] [data-disclosure-row]{z-index:1;background:var(--dsw-alias-bg-base);position:sticky;top:0}",
1079
+ '.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}',
1080
+ "@keyframes dsh-ct-reasoning-sweep{0%{left:-300px}90%,to{left:100%}}",
1081
+ "@media (prefers-reduced-motion:reduce){.dsh-ct-think-root[data-state=running] .dsh-ct-think-row:after{animation:none}}",
1082
+ ".dsh-ct-think-leading{flex-shrink:0}",
1083
+ ".dsh-ct-think-chevron{color:var(--dsw-alias-label-secondary)}",
1084
+ ".dsh-ct-think-title{font-weight:400}",
1085
+ ".dsh-ct-think-separator{background:var(--dsw-alias-label-caption);border-radius:1px;flex:none;width:2px;height:2px;margin:0 8px}",
1086
+ ".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}",
1087
+ ".dsh-ct-think-summary-text{text-overflow:ellipsis;display:block;overflow:hidden}",
1088
+ ".dsh-ct-think-summary[data-streaming]{mask-image:linear-gradient(90deg,currentcolor calc(100% - 48px),transparent)}",
1089
+ ".dsh-ct-think-summary[data-streaming] .dsh-ct-think-summary-text{text-overflow:clip;overflow:visible}",
1090
+ ".dsh-ct-think-root:not([data-preview]) .dsh-ct-think-separator,.dsh-ct-think-root:not([data-preview]) .dsh-ct-think-summary{display:none}",
1091
+ ".dsh-ct-think-body{padding:4px 0 4px calc(22px + var(--dsh-content-font-delta,0px));min-width:0}",
1092
+ // ---- 无障碍:屏幕阅读器专用文本 ----
1093
+ ".dsh-ct-visually-hidden{clip:rect(0 0 0 0);white-space:nowrap;width:1px;height:1px;position:absolute;overflow:hidden}"
1094
+ ].join("");
1095
+ var PROSE_BY_MARK = {
1096
+ translated: { className: "dsh-ct-prose-translated", action: "toggle" },
1097
+ "original-view": { className: "dsh-ct-prose-original", action: "toggle" },
1098
+ failed: { className: "dsh-ct-prose-failed", action: "retry" },
1099
+ // 在途有线、有热区,但 action 为 null:按下去是空操作(脉动已是「已在跑」
1100
+ // 的答复),热区保留只是为了让键盘焦点不掉回页面。
1101
+ inflight: { className: "dsh-ct-prose-inflight", action: null }
1102
+ };
1103
+ var PROSE_BASE_CLASS = "dsh-ct-prose";
1104
+ function prosePresentation(mark) {
1105
+ return mark === null ? { className: null, action: null } : PROSE_BY_MARK[mark];
1106
+ }
1107
+ function proseClassNames(mark) {
1108
+ const line = prosePresentation(mark).className;
1109
+ return line === null ? void 0 : `${PROSE_BASE_CLASS} ${line}`;
1110
+ }
1111
+ function proseIndentClassName(canTranslate) {
1112
+ return canTranslate ? "dsh-ct-prose-indent" : void 0;
1113
+ }
1114
+ var HOTSPOT_CLASS = "dsh-ct-hotspot";
1115
+ var HOTSPOT_IDLE_ATTR = "data-idle";
1116
+ function ensureAssistantStyles() {
1117
+ if (typeof document === "undefined") return;
1118
+ const selector = `style[data-plugin-css=${JSON.stringify(STYLE_TAG_ID)}]`;
1119
+ if (document.querySelector(selector) !== null) return;
1120
+ const tag = document.createElement("style");
1121
+ tag.dataset.plugin = "@lynn123411/dsh-chat-translate";
1122
+ tag.dataset.pluginCss = STYLE_TAG_ID;
1123
+ tag.textContent = ASSISTANT_CSS;
1124
+ document.head.appendChild(tag);
1125
+ }
1126
+
909
1127
  // src/server/pipeline/blocks.ts
910
1128
  var REPLY_MAX_INPUT_TOKENS = 4096;
911
1129
  var CJK_CHAR = /[\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff\u3000-\u303f\uff00-\uffef]/;
@@ -919,7 +1137,6 @@ function estimateTokens(text) {
919
1137
 
920
1138
  // src/client/chat/translate-store.ts
921
1139
  var MAX_TRANSLATED_ROWS = 200;
922
- var MAX_ROW_ATTEMPTS = 3;
923
1140
  var REQUEST_TOKEN_BUDGET = REPLY_MAX_INPUT_TOKENS;
924
1141
  function chunkTexts(texts) {
925
1142
  const batches = [];
@@ -966,32 +1183,33 @@ var ChatTranslateStore = class {
966
1183
  }
967
1184
  /**
968
1185
  * 确保该行按当前文本被翻译。
969
- * @returns 是否发出了新的请求(同代 done/pending 与换代前的登记之外都是 false)。
1186
+ * @param manual - 用户点击失败块的左缘热区发起的补跑:已落定的同代行只有这条路会重发。
1187
+ * @returns 是否发出了新的请求。
970
1188
  */
971
- ensure(rowKey, texts) {
1189
+ ensure(rowKey, texts, manual = false) {
972
1190
  const current = this.rows.get(rowKey);
973
- let attempts = 1;
1191
+ let seed = null;
974
1192
  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;
1193
+ if (current.status === "pending") return false;
1194
+ if (!manual) return false;
1195
+ seed = current.outcomes.map((outcome) => outcome !== null && outcome.ok ? { ...outcome } : null);
978
1196
  }
979
1197
  const snapshot = [...texts];
1198
+ const seedOutcomes = seed ?? snapshot.map(() => null);
980
1199
  const next = {
981
1200
  status: "pending",
982
1201
  texts: snapshot,
983
- outcomes: snapshot.map(() => null),
984
- attempts
1202
+ outcomes: seedOutcomes
985
1203
  };
986
1204
  this.rows.delete(rowKey);
987
1205
  this.rows.set(rowKey, next);
988
1206
  this.prune();
989
1207
  this.bump();
990
- if (snapshot.length > 0) void this.run(rowKey, snapshot);
1208
+ if (snapshot.length > 0) void this.run(rowKey, snapshot, seedOutcomes);
991
1209
  return true;
992
1210
  }
993
- async run(rowKey, texts) {
994
- const outcomes = texts.map(() => null);
1211
+ async run(rowKey, texts, initial) {
1212
+ const outcomes = initial.map((o) => o);
995
1213
  let offset = 0;
996
1214
  for (const batch of chunkTexts(texts)) {
997
1215
  let results = [];
@@ -1005,7 +1223,12 @@ var ChatTranslateStore = class {
1005
1223
  if (result !== void 0 && result.ok && result.translated && result.translated.trim()) {
1006
1224
  outcomes[offset + i] = { translated: result.translated, ok: true };
1007
1225
  } else {
1008
- outcomes[offset + i] = { translated: batch[i] ?? texts[offset + i] ?? "", ok: false };
1226
+ const detail = typeof result?.detail === "string" && result.detail !== "" ? result.detail : void 0;
1227
+ outcomes[offset + i] = {
1228
+ translated: batch[i] ?? texts[offset + i] ?? "",
1229
+ ok: false,
1230
+ ...detail === void 0 ? {} : { detail }
1231
+ };
1009
1232
  }
1010
1233
  }
1011
1234
  offset += batch.length;
@@ -1014,19 +1237,16 @@ var ChatTranslateStore = class {
1014
1237
  this.rows.set(rowKey, {
1015
1238
  status: "pending",
1016
1239
  texts: live.texts,
1017
- outcomes: [...outcomes],
1018
- attempts: live.attempts
1240
+ outcomes: [...outcomes]
1019
1241
  });
1020
1242
  this.bump();
1021
1243
  }
1022
1244
  const state = this.rows.get(rowKey);
1023
1245
  if (state?.status === "pending" && sameTexts(state.texts, texts)) {
1024
- const anyFailed = outcomes.some((outcome) => outcome === null || !outcome.ok);
1025
1246
  this.rows.set(rowKey, {
1026
- status: anyFailed ? "partial" : "done",
1247
+ status: "settled",
1027
1248
  texts: state.texts,
1028
- outcomes,
1029
- attempts: state.attempts
1249
+ outcomes: [...outcomes]
1030
1250
  });
1031
1251
  this.bump();
1032
1252
  }
@@ -1061,133 +1281,6 @@ function createTranslateStore(fetcher) {
1061
1281
  }
1062
1282
  var chatTranslate = createTranslateStore(requestTranslateReply);
1063
1283
 
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
1284
  // src/client/chat/assistant-step.tsx
1192
1285
  var thinkIcon = () => React.createElement(import_dsh_client_ui_primitives3.IconThinkOutlineRegular, { size: 14 });
1193
1286
  function markdownLabels(t) {
@@ -1352,44 +1445,69 @@ var ReasoningRow = (0, import_react4.memo)(function ReasoningRow2({
1352
1445
  )
1353
1446
  );
1354
1447
  });
1355
- function blockClick(event, onToggle) {
1356
- const guardEvent = {
1357
- target: event.target,
1358
- currentTarget: event.currentTarget
1359
- };
1360
- if (isBareBlockClick(guardEvent, typeof window === "undefined" ? void 0 : window)) onToggle();
1361
- }
1362
1448
  function ProseBlock({
1363
1449
  text,
1364
1450
  translated,
1365
- showOriginal,
1451
+ mark,
1452
+ fail,
1366
1453
  onToggle,
1454
+ onRetry,
1455
+ canTranslate,
1367
1456
  streaming,
1368
1457
  labels,
1369
1458
  mentions,
1370
1459
  pathImages
1371
1460
  }) {
1372
- const processed = translated !== null;
1373
- const showTranslation = processed && !showOriginal;
1374
- const source = showTranslation ? translated : text;
1461
+ const { action } = prosePresentation(mark);
1462
+ const clickable = action !== null;
1463
+ const retrying = action === "retry";
1464
+ const handler = retrying ? onRetry : onToggle;
1465
+ const source = mark === "translated" ? translated : text;
1466
+ const copy = rowCopy();
1467
+ const className = [proseClassNames(mark) ?? PROSE_BASE_CLASS, proseIndentClassName(canTranslate)].filter((name2) => name2 !== void 0).join(" ");
1468
+ const hotspot = mark === null ? null : React.createElement("button", {
1469
+ type: "button",
1470
+ className: HOTSPOT_CLASS,
1471
+ // 热区是覆盖层,读屏只应听到它的动作,正文仍由 MarkdownText 读。
1472
+ "aria-label": mark === "inflight" ? copy.inflightAria : retrying ? copy.retryAria : mark === "original-view" ? copy.toggleToTranslatedAria : copy.toggleToOriginalAria,
1473
+ ...mark === "inflight" ? { [HOTSPOT_IDLE_ATTR]: "true" } : {},
1474
+ tabIndex: 0,
1475
+ // button 自己会按 Enter/Space 合成 click,若不接管就会与这段键盘处理
1476
+ // 叠加成两次动作:这里两个键都走 keydown 并 preventDefault,合成被掐掉。
1477
+ onKeyDown: clickable ? (event) => {
1478
+ if (event.key !== "Enter" && event.key !== " ") return;
1479
+ event.preventDefault();
1480
+ handler();
1481
+ } : void 0,
1482
+ onClick: clickable ? (event) => {
1483
+ if (isCleanHotspotClick(
1484
+ { target: event.target, currentTarget: event.currentTarget },
1485
+ typeof window === "undefined" ? void 0 : window
1486
+ )) {
1487
+ handler();
1488
+ }
1489
+ } : void 0
1490
+ });
1491
+ const retryIcon = retrying ? React.createElement(
1492
+ "span",
1493
+ { className: "dsh-ct-retry", "aria-hidden": true },
1494
+ React.createElement(import_dsh_client_ui_primitives3.IconRefreshOutlineRegular, { size: 10 })
1495
+ ) : null;
1496
+ const anchor = hotspot === null ? null : React.createElement(import_dsh_client_ui_primitives3.Tooltip, {
1497
+ label: () => copy.failTitle(fail),
1498
+ side: "top",
1499
+ portal: true,
1500
+ disabled: !retrying,
1501
+ children: hotspot
1502
+ });
1375
1503
  return React.createElement(
1376
1504
  "div",
1377
1505
  {
1378
- // 色相+粗细随显示态:蓝粗线=译文,灰细线=有译文但读原文(仍可点切回),无线=没译。
1379
- className: proseClassNames(processed, showOriginal),
1380
- "data-translated": showTranslation ? "true" : void 0,
1381
- // 容器内含链接等交互内容,不套 role=button(非法嵌套);可聚焦 + 键盘
1382
- // Enter/Space 即切换,满足官方「键盘可达」门。
1383
- tabIndex: processed ? 0 : void 0,
1384
- onClick: processed ? (event) => blockClick(event, onToggle) : void 0,
1385
- onKeyDown: processed ? (event) => {
1386
- if (event.target !== event.currentTarget) return;
1387
- if (event.key === "Enter" || event.key === " ") {
1388
- event.preventDefault();
1389
- onToggle();
1390
- }
1391
- } : void 0
1506
+ className,
1507
+ "data-translated": mark === "translated" ? "true" : void 0
1392
1508
  },
1509
+ retryIcon,
1510
+ anchor,
1393
1511
  React.createElement(import_dsh_client_ui_primitives3.MarkdownText, {
1394
1512
  text: source,
1395
1513
  streaming,
@@ -1441,7 +1559,9 @@ var AssistantStepView = (0, import_react4.memo)(function AssistantStepView2(prop
1441
1559
  if (streaming || !canTranslate || !inView || plan.entries === null || plan.texts.length === 0) return;
1442
1560
  chatTranslate.ensure(rowKey, plan.texts);
1443
1561
  }, [streaming, canTranslate, inView, rowKey, plan]);
1444
- const rowState = chatTranslate.getState(rowKey);
1562
+ const [originalKeys, setOriginalKeys] = (0, import_react4.useState)(() => /* @__PURE__ */ new Set());
1563
+ const registered = chatTranslate.getState(rowKey);
1564
+ const rowState = registered !== void 0 && sameTexts(registered.texts, plan.texts) ? registered : void 0;
1445
1565
  const displayed = (0, import_react4.useMemo)(
1446
1566
  () => planAssistantRow({
1447
1567
  blocks,
@@ -1449,15 +1569,16 @@ var AssistantStepView = (0, import_react4.memo)(function AssistantStepView2(prop
1449
1569
  interrupted,
1450
1570
  groupPart,
1451
1571
  canTranslate,
1452
- outcomes: rowState?.outcomes
1572
+ outcomes: rowState?.outcomes,
1573
+ rowStatus: rowState?.status,
1574
+ originalKeys
1453
1575
  }),
1454
- [blocks, streaming, interrupted, groupPart, canTranslate, rowState]
1576
+ [blocks, streaming, interrupted, groupPart, canTranslate, rowState, originalKeys]
1455
1577
  );
1456
1578
  const reasoningHidden = turnProcess !== void 0 && turnProcess.foldable && turnProcess.spec.answerStep === data.step && turnProcess.spec.inlineReasoning && !turnProcess.open;
1457
1579
  const revealProcess = (0, import_react4.useCallback)(() => {
1458
1580
  turnProcess?.setOpen(true);
1459
1581
  }, [turnProcess]);
1460
- const [originalKeys, setOriginalKeys] = (0, import_react4.useState)(() => /* @__PURE__ */ new Set());
1461
1582
  const toggleBlock = (0, import_react4.useCallback)((key) => {
1462
1583
  setOriginalKeys((current) => {
1463
1584
  const next = new Set(current);
@@ -1466,6 +1587,9 @@ var AssistantStepView = (0, import_react4.memo)(function AssistantStepView2(prop
1466
1587
  return next;
1467
1588
  });
1468
1589
  }, []);
1590
+ const retryRow = (0, import_react4.useCallback)(() => {
1591
+ chatTranslate.ensure(rowKey, plan.texts, true);
1592
+ }, [rowKey, plan]);
1469
1593
  if (displayed.entries === null) return null;
1470
1594
  const last = blocks.length - 1;
1471
1595
  const rendered = [];
@@ -1477,8 +1601,11 @@ var AssistantStepView = (0, import_react4.memo)(function AssistantStepView2(prop
1477
1601
  key: entry.key,
1478
1602
  text: entry.text,
1479
1603
  translated: entry.translated,
1480
- showOriginal: originalKeys.has(entry.key),
1604
+ mark: entry.mark,
1605
+ fail: entry.fail,
1481
1606
  onToggle: () => toggleBlock(entry.key),
1607
+ onRetry: retryRow,
1608
+ canTranslate,
1482
1609
  streaming,
1483
1610
  labels,
1484
1611
  mentions,
@@ -1648,6 +1775,7 @@ function apply(ctx) {
1648
1775
  }, "dsh-chat-translate: chat presentation source");
1649
1776
  ctx.effect(() => setupSettingsUi(ctx), "dsh-chat-translate: settings section");
1650
1777
  ctx.effect(() => setupComposerToggle(ctx), "dsh-chat-translate: composer dock toggle");
1778
+ bindRowCopy(ctx.locale);
1651
1779
  }
1652
1780
  return module.exports; } });
1653
1781
  //# sourceMappingURL=client.js.map