@lynn123411/dsh-chat-translate 3.1.0 → 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/README.md CHANGED
@@ -1,15 +1,15 @@
1
1
  # @lynn123411/dsh-chat-translate
2
2
 
3
- DeepSeek Harness Web 界面的**助手回答正文翻译**插件。它接管助手行的渲染:正文块不论源语言,一律送翻译模型加工成自然简体中文(如 `I traced the failing path to a stale lock file.` → 中文;模型写出的生硬机器腔中文同样送一遍润色——弱模型的中文恰恰最需要加工),译文按原 markdown 结构重新渲染。只翻正文:Think 卡的思考链、工具调用行、用户消息、代码块、空白块不动。正文块的左缘竖线报出状态:译文蓝粗线、备着译文的原文灰细线、失败红实线(悬停报通道细节:超时、断流、空返回)、在途灰脉动、没送过模型无线;成功的块点击切双语,失败的块点击即手动补跑。不污染会话上下文。开关有两个入口:输入框下方那一行的「译」胶囊与共享「阅读体验」设置页的总开关,同一个状态两处同步。配置详见 [docs/配置.md](./docs/配置.md)。
3
+ DeepSeek Harness Web 界面的**助手回答正文翻译**插件。它接管助手行的渲染:正文块不论源语言,一律送翻译模型加工成自然简体中文(如 `I traced the failing path to a stale lock file.` → 中文;模型写出的生硬机器腔中文同样送一遍润色——弱模型的中文恰恰最需要加工),译文按原 markdown 结构重新渲染。只翻正文:Think 卡的思考链、工具调用行、用户消息、代码块、空白块不动。正文块的左缘竖线报出状态:译文蓝粗线、备着译文的原文灰细线、失败红实线(悬停报通道细节:超时、断流、空返回)、在途灰脉动、没送过模型无线。切换与补跑都在**贴着那条线的窄带上**(鼠标移上去变手型,点一下切双语;挂红线的块点它手动补跑),正文整块不再响应点击——选中文字、拖选复制都不会误触。不污染会话上下文。开关有两个入口:输入框下方那一行的「译」胶囊与共享「阅读体验」设置页的总开关,同一个状态两处同步。配置详见 [docs/配置.md](./docs/配置.md)。
4
4
 
5
5
  ## 特性
6
6
 
7
7
  - **接管助手行渲染**:经 keyed slot `conversation.chat.node`(key `assistant-step`,priority -1)替换宿主的助手行渲染器。正文块从数据层拿**原始 markdown**(`AssistantBlock.text`),译文同样以 markdown 交给宿主公开的基线组件 `MarkdownText`(micromark + GFM + KaTeX)重渲染——表格、列表、加粗、行内代码、公式的结构由官方渲染器保证,插件不自拼 markup。接管渲染器抛错时按框架退位规则退出该 cell,宿主原渲染器自动回位。
8
8
  - **一律送译,不判源语言**:正文的每一块都进模型,包括整段中文、纯数字、纯标点的块;只有 trim 后为空的块不送译、永不挂线(它没送过模型,重试也永远不会成,挂线只会留下消不掉的标记)。提示词要求模型把每一段都改写成自然、地道的简体中文——意义、语气、专名、数字与术语不动,不增不删;同时要求 markdown 记号原样保留。模型判断某段已经最好而返回一模一样的文字时,同样算翻译过、同样挂线(标记传达的是「这段过了模型」)。
9
9
  - **除正文外与宿主逐分支等价**:Think 折叠行(含工作细节策略下的摘要预览、Turn-process 折叠隐藏与页内搜索揭示)、连续图片组交回宿主渲染、工具调用行由宿主独立渲染、未知内容块落 JSON 展示、中断前缀行尾带「已停止」——这些 chrome 文案复用宿主 `chat` 词典,两侧逐字一致。
10
- - **左缘竖线报状态,点击即操作**:显示译文为 1px 主色蓝线、点回原文为 0.5px 中性灰线(留有译文、可再点回的线索)、失败一律为 1px error 色红实线(不分败因——区别在悬停文案里说清)、在途为灰线脉动、没送过模型无线(区分走色相与粗细)。有蓝灰红线的块整块可点击(键盘 Enter/Space 同样):成功的块在译文与原文间切换,失败的块发起手动补跑,块与块互不影响,关闭开关即全部回到原文、线全部撤下。
10
+ - **左缘竖线报状态,贴着线的那条窄带才可点**:显示译文为 1px 主色蓝线、点回原文为 0.5px 中性灰线(留有译文、可再点回的线索)、失败一律为 1px error 色红实线(不分败因——区别在悬停文案里说清)、在途为灰线脉动、没送过模型无线(区分走色相与粗细)。可点的是左缘那条约 17px 的窄带(线外侧 4px + 线 + 线内侧那 12px 缩进,随块高铺满),不是正文:鼠标移上去变手型,点一下即操作——成功的块在译文与原文间切换,失败的块发起手动补跑,块与块互不影响,关闭开关即全部回到原文、线全部撤下。悬停不改任何视觉,只有键盘聚焦时线加亮(粗细始终只表示译文/原文);正文整块不响应点击,拖选复制不再误触切换。可翻译时(开关开 + 通道齐备)所有正文块统一左缩进 12px,译文落定时正文不跳字;关掉开关或通道没配好则不缩进,排版回到宿主原样。
11
11
  - **落定才翻、进视口才翻**:回答流式期间整行按原文渲染;`data-streaming` 落定且行进入视口(150px 缓冲)后,整行正文按估算 token 预算切批、逐批顺序请求——先回的段先出中文,按阅读顺序逐段呈现。历史会话因此只在真的被读到时才花请求。这是每行的首跑,也是唯一自动发生的请求。
12
- - **失败只有一种:通道伤**:块级失败不进缓存、保持原文,左缘挂红实线,悬停报出通道细节(超时、断流、空返回)。译文的结构漂移不再是失败理由(见下条)。红线块可点:整行手动补跑,已成功的块保持挂线展示(宿主磁盘缓存让它们秒回,不花模型调用),只有尚无译文的块重新脉动;在途时再点是空操作。通道失败不自动重试:一行首跑一次,落定即终态,救活由用户发起。失败仍在控制台留一条警告。
12
+ - **失败只有一种:通道伤**:块级失败不进缓存、保持原文,左缘挂红实线,悬停左缘那条窄带报出通道细节(超时、断流、空返回)。译文的结构漂移不再是失败理由(见下条)。红线块右侧常驻一个小 ↻,点那条窄带即整行手动补跑,已成功的块保持挂线展示(宿主磁盘缓存让它们秒回,不花模型调用),只有尚无译文的块重新脉动;在途时窄带仍在(键盘焦点不掉)但按下去是空操作,光标也不再变手型。通道失败不自动重试:一行首跑一次,落定即终态,救活由用户发起。失败仍在控制台留一条警告。
13
13
  - **按行幂等的翻译池**:客户端按会话锚点键登记整行结果,同文本重复渲染不重发请求;行内容换代则整行重译,迟到的旧代结果作废。
14
14
  - **结构重装配,形状问题只修不拒**:宿主先把块按 markdown 结构切段——代码围栏与纯空白段**逐字保留、永不送模型**——散文段再按输入上限切片(单段超限按空行、单行、句末切开)、相邻片段打包成一个请求(输入上限 4096 估算 token、输出上限 8192 token)。回来的译文过**形状修复**(`repairShape`):按段落块对齐,块内结构行(记号类别、表格竖线数、链接个数、缩进代码缩进)逐一对齐并把行首前缀修回原文同款(`##`→`###`、`-`→`*`、序号、引用深度、列表缩进的漂移就此消失),散文行自由重排(软换行渲染无感),空行布局一律取原文,`[文字](URL)` 的 URL 按出现序逐字拼回。修不齐的片段自动重掷一次;仍不齐就照收模型的译文——原文一键可回,可读的译文优先于红线。反引号、行内代码内容一律不拦:是样式不是骨架。
15
15
  - **单通道:AI 翻译(OpenAI 兼容协议)**:对接任意 OpenAI 兼容的 `chat/completions` 服务(OpenAI、DeepSeek、通义、Ollama、本地 vLLM 等)。Key / Base URL / 模型三者齐备才翻译,缺任何一项保持原文,不做降级兜底。API Key 经 DSH 凭据服务读写 `~/.dsh/.credentials.yaml` 的 `TRANSLATE_API_KEY`;Base URL 与模型在设置面板配置。
@@ -25,12 +25,13 @@
25
25
 
26
26
  目标语言固定为简体中文,没有配置项。
27
27
 
28
- 3. **翻译范围与时机**:插件接管助手行的渲染(keyed slot `conversation.chat.node` / `assistant-step`,priority -1)。正文块不论源语言一律送模型改写成自然简体中文——「用时 XXX」大折叠块里每一步露出的正文与最终汇总那一步一视同仁;整段中文同样照送(意义、术语、数字不动,顺掉机器腔)。模型返回与原文一模一样的文字时同样算翻译过:照常显示、照常可切换。trim 后为空的块不送译、不占下标、永不挂线;块内的代码围栏逐字保留、从不送模型。左缘线的色相、线型与粗细随显示态:显示译文挂 1px 主色蓝线(`--dsw-alias-state-business-primary` 65%),点回原文挂 0.5px 中性灰线(`--dsw-alias-border-l2`,备着译文、可点回),失败一律挂 1px error 色红实线(`--dsw-alias-state-error-primary` 65%——只剩通道伤一种,悬停报细节),在途挂灰线脉动(`prefers-reduced-motion` 降级为静态),没送过模型无线;线宽之差亚像素级,切换不跳字。分组判据是座次传入的 `groupPart`(`response` 分组排除思考块,`reasoning` 分组只渲染思考块,不送译),流式判据是行数据的 `status === "running"`。流式期间整行按原文渲染;落定且该行进入视口(150px 缓冲)后,整行正文按估算 token 预算切批、逐批顺序请求,先回的段先出中文——这是每行的首跑,也是唯一自动发生的请求。
28
+ 3. **翻译范围与时机**:插件接管助手行的渲染(keyed slot `conversation.chat.node` / `assistant-step`,priority -1)。正文块不论源语言一律送模型改写成自然简体中文——「用时 XXX」大折叠块里每一步露出的正文与最终汇总那一步一视同仁;整段中文同样照送(意义、术语、数字不动,顺掉机器腔)。模型返回与原文一模一样的文字时同样算翻译过:照常显示、照常可切换。trim 后为空的块不送译、不占下标、永不挂线;块内的代码围栏逐字保留、从不送模型。左缘线的色相、线型与粗细随显示态:显示译文挂 1px 主色蓝线(`--dsw-alias-state-business-primary` 65%),点回原文挂 0.5px 中性灰线(`--dsw-alias-border-l2`,备着译文、可点回),失败一律挂 1px error 色红实线(`--dsw-alias-state-error-primary` 65%——只剩通道伤一种,悬停报细节),在途挂灰线脉动(`prefers-reduced-motion` 降级为静态),没送过模型无线;线宽之差亚像素级,切换不跳字。可点的是贴着这条线的窄带而不是正文,详见下条。分组判据是座次传入的 `groupPart`(`response` 分组排除思考块,`reasoning` 分组只渲染思考块,不送译),流式判据是行数据的 `status === "running"`。流式期间整行按原文渲染;落定且该行进入视口(150px 缓冲)后,整行正文按估算 token 预算切批、逐批顺序请求,先回的段先出中文——这是每行的首跑,也是唯一自动发生的请求。
29
29
 
30
30
  - 不送译的内容:Think 卡的思考链正文、工具调用行、用户消息、代码块(`pre`)及其内部、空白块——这些与宿主渲染逐分支等价,接管渲染器抛错时按框架退位规则由宿主渲染器自动回位。
31
31
  - 译文用宿主基线组件 `MarkdownText` 重新渲染,表格、列表、加粗、行内代码的结构保持原样。
32
- - 失败只有一种(通道伤)、悬停报细节,重试归手点:块内任一片段通道失败(超时、断流、空返回)就整块按原文渲染、挂 1px error 色红实线;悬停出 Tooltip 报服务端原样带回的技术一句(如展开后的 fetch 报错、`channel timed out after 20000ms`)。**译文的结构漂移不是失败理由**——它由形状修复消化(见下条),修不齐的自动重掷一次、仍不齐则照收,永不因格式挂红线。控制台各留一条通道失败警告。失败结果不进缓存,一批失败只败在这一批;通道失败**不自动重试**——滚动、重挂载都不会重发已落定的行,救活由点击发起:点任一红线块 = 整行手动补跑,无限次不设额度,已成功的块当场种回保持挂线(宿主磁盘缓存让它们秒回、不花模型调用),只有尚无译文的块重新脉动;在途再点是空操作。
33
- - 点击翻译过的块在译文与原文间切换,点击挂红线的块补跑整行(键盘 Enter/Space 同样),块与块互不影响;关闭总开关即全部回到原文、线全部撤下。悬停或键盘聚焦失败块时,左缘线槽里露出小 ↻(官方 `IconRefreshOutlineRegular`),Tooltip 报的就是上述败因而非操作指引,屏幕阅读器读「翻译失败,按 Enter 重试」。
32
+ - 失败只有一种(通道伤)、悬停报细节,重试归手点:块内任一片段通道失败(超时、断流、空返回)就整块按原文渲染、挂 1px error 色红实线;悬停左缘那条窄带出 Tooltip,报服务端原样带回的技术一句(如展开后的 fetch 报错、`channel timed out after 20000ms`)。**译文的结构漂移不是失败理由**——它由形状修复消化(见下条),修不齐的自动重掷一次、仍不齐则照收,永不因格式挂红线。控制台各留一条通道失败警告。失败结果不进缓存,一批失败只败在这一批;通道失败**不自动重试**——滚动、重挂载都不会重发已落定的行,救活由点击发起:点任一红线块的左缘窄带 = 整行手动补跑,无限次不设额度,已成功的块当场种回保持挂线(宿主磁盘缓存让它们秒回、不花模型调用),只有尚无译文的块重新脉动;在途窄带仍在(键盘焦点不掉)但按下去是空操作,光标也不再变手型。
33
+ - 可点的是**贴着左缘线的那条约 17px 窄带**(线外侧 4px + 线 + 线内侧那 12px 缩进,随块高铺满),不是正文:点它时读译文切原文、读原文切译文,挂红线的块则由它补跑整行(键盘聚焦后 Enter/Space 同样),块与块互不影响;关闭总开关即全部回到原文、线全部撤下。正文整块不再响应点击——选中文字、拖选复制都不会误触;在窄带上按下、往右拖进正文再松手算选字,不触发切换。悬停不改任何视觉(鼠标移到窄带上才变手型),只有键盘聚焦时线加亮,粗细始终只表示译文/原文。挂红线的块右侧常驻一个小 ↻(官方 `IconRefreshOutlineRegular`,纯装饰不吃点击),不靠悬停显形;Tooltip 报的就是上述败因而非操作指引,屏幕阅读器读「翻译失败,点左缘那条窄带可按 Enter 重试」。
34
+ - **缩进**:能翻译时(开关开 + Key / Base URL / 模型齐备)所有正文块统一左缩进 12px——与这块是否已挂线无关,译文落定的那一刻正文不右移、块与块左缘对齐;关掉开关或通道没配好则不缩进,排版回到宿主原样(此时切换开关整列会横移一次,是刻意的取舍)。同一行的思考行、图片组、未知块与「已停止」标记不缩进,与宿主逐字一致。
34
35
 
35
36
  4. **请求方式**:按行请求——落定的助手行把它全部正文块一次送出。宿主先按 markdown 结构切段:代码围栏与纯空白段逐字保留、永不送模型;散文段超过输入上限(4096 估算 token)时按空行、单行、句末依次切开,相邻片段打包成一个请求(输出上限 8192 token),译回后按原结构拼回——片段头尾的空行由拆下的部分补回,`[文字](URL)` 的 URL 按出现序逐字拼回。每片段过**形状修复**(`repairShape`):按段落块对齐,块内结构行(记号类别、表格竖线数、链接个数、缩进代码缩进)逐一对齐并把行首前缀修回原文同款(标题层级、列表符号、序号、引用深度、列表缩进的漂移就此消失),散文行自由重排(软换行渲染无感、只查整块链接总数不丢),空行布局一律取原文。修不齐的片段自动重掷一次;仍不齐则照收模型的译文(链接数不等时不回填,其余原样)——形状问题永不否决内容,红线只留给通道伤。同时最多一个在途请求,超时由 `aiTimeoutMs` 控制(默认 600000 毫秒,范围 500–900000)——每行首跑与每次手点都等到真实结果,嫌等就调小超时。客户端只保留一个按行的内存翻译池(会话锚点为键,LRU 200 行);持久缓存只有宿主的磁盘池 `~/.dsh/dsh-chat-translate/cache.json`(1000 条、7 天 LRU),文档形态是 `{ rev, entries }`,其中 `rev` 登记写入它的提示词修订号——修订号(`src/server/prompt-revision.ts` 的 `PROMPT_REVISION`,单点登记)与实际加载的提示词不符时整池作废重来,换提示词不会让旧译文串代。
36
37
 
package/lib/client.js CHANGED
@@ -544,7 +544,7 @@ var zh = {
544
544
  title: "\u6B63\u6587\u7FFB\u8BD1",
545
545
  masterTitle: "\u7FFB\u8BD1\u603B\u5F00\u5173",
546
546
  enableTranslation: "\u542F\u7528\u7FFB\u8BD1",
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",
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",
548
548
  aiTitle: "AI \u7FFB\u8BD1\uFF08OpenAI \u517C\u5BB9\u534F\u8BAE\uFF09",
549
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",
550
550
  badgeConfigured: "\u5DF2\u914D\u7F6E",
@@ -574,14 +574,17 @@ var zh = {
574
574
  dockOn: "\u6B63\u6587\u7FFB\u8BD1\u5DF2\u5F00\u542F\uFF0C\u70B9\u51FB\u5173\u95ED",
575
575
  dockOff: "\u6B63\u6587\u7FFB\u8BD1\u5DF2\u5173\u95ED\uFF0C\u70B9\u51FB\u5F00\u542F",
576
576
  failTitle: "\u7FFB\u8BD1\u5931\u8D25",
577
- retryAria: "\u7FFB\u8BD1\u5931\u8D25\uFF0C\u6309 Enter \u91CD\u8BD5"
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"
578
581
  };
579
582
  var en = {
580
583
  pageNav: "Reading",
581
584
  title: "Reply translate",
582
585
  masterTitle: "Translation master switch",
583
586
  enableTranslation: "Enable translation",
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.",
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.",
585
588
  aiTitle: "AI translation (OpenAI-compatible)",
586
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.",
587
590
  badgeConfigured: "Configured",
@@ -611,7 +614,10 @@ var en = {
611
614
  dockOn: "Translation is on; click to turn it off",
612
615
  dockOff: "Translation is off; click to turn it on",
613
616
  failTitle: "Translation failed",
614
- retryAria: "Translation failed \u2014 press Enter to retry"
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"
615
621
  };
616
622
  var rowT = (key) => zh[key] ?? key;
617
623
  function bindRowCopy(locale) {
@@ -623,6 +629,9 @@ function bindRowCopy(locale) {
623
629
  function rowCopy() {
624
630
  return {
625
631
  retryAria: rowT("retryAria"),
632
+ toggleToOriginalAria: rowT("toggleToOriginalAria"),
633
+ toggleToTranslatedAria: rowT("toggleToTranslatedAria"),
634
+ inflightAria: rowT("inflightAria"),
626
635
  failTitle: (detail) => {
627
636
  const label = rowT("failTitle");
628
637
  return detail === void 0 || detail === "" || detail === null ? label : `${label} \u2014 ${detail}`;
@@ -1005,10 +1014,8 @@ function planAssistantRow(input) {
1005
1014
  }
1006
1015
 
1007
1016
  // 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;
1017
+ function isCleanHotspotClick(event, view) {
1018
+ if (event.target !== event.currentTarget) return false;
1012
1019
  const selection = typeof view?.getSelection === "function" ? view.getSelection() : null;
1013
1020
  if (selection !== null && selection !== void 0) {
1014
1021
  if (selection.isCollapsed === false && String(selection.toString?.() ?? "") !== "") return false;
@@ -1028,25 +1035,42 @@ var ASSISTANT_CSS = [
1028
1035
  ".dsh-ct-body .md-table-wide>table{z-index:1;position:relative}",
1029
1036
  // ---- 已停止标记(宿主 .hWmORq_stopped 等价) ----
1030
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}",
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)}",
1038
+ // ---- 正文块:可翻译时左缘留出 12px 缩进(缩进与线无关——译文出现前后
1039
+ // 正文位置不变,切换总开关时整列也不横移),并成为热区的定位父级 ----
1040
+ ".dsh-ct-prose{position:relative}",
1041
+ ".dsh-ct-prose-indent{padding-left:12px}",
1042
+ // ---- 线型:粗细与色相只表示状态。蓝粗线=读译文,灰细线=读原文备着译文
1043
+ // (0.5px 中性 hairline),红实线=失败(只剩通道伤一种) ----
1035
1044
  ".dsh-ct-prose-translated{border-left:1px solid color-mix(in srgb, var(--dsw-alias-state-business-primary) 65%, transparent)}",
1036
1045
  ".dsh-ct-prose-original{border-left:0.5px solid var(--dsw-alias-border-l2)}",
1037
- // ---- 失败标记:1px error 色红实线(只剩通道伤一种,悬停报细节);
1038
- // 可点,点=整行手动补跑 ----
1039
1046
  ".dsh-ct-prose-failed{border-left:1px solid color-mix(in srgb, var(--dsw-alias-state-error-primary) 65%, transparent)}",
1040
- // ---- 在途:灰 1px 线脉动(与成功态同 padding,转蓝不跳字);
1047
+ // ---- 在途:灰 1px 线脉动(缩进由 .dsh-ct-prose-indent 统一给,转蓝不跳字);
1041
1048
  // 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}",
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}",
1043
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)}}",
1044
1051
  "@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}",
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}",
1050
1074
  // ---- ReasoningRow(宿主 lcKema_* 等价,前缀换成 dsh-ct-think) ----
1051
1075
  ".dsh-ct-think-root{flex-direction:column;display:flex}",
1052
1076
  ".dsh-ct-think-root:not([data-expanded]){contain:size layout;height:calc(24px + var(--dsh-content-font-delta,0px))}",
@@ -1068,31 +1092,27 @@ var ASSISTANT_CSS = [
1068
1092
  // ---- 无障碍:屏幕阅读器专用文本 ----
1069
1093
  ".dsh-ct-visually-hidden{clip:rect(0 0 0 0);white-space:nowrap;width:1px;height:1px;position:absolute;overflow:hidden}"
1070
1094
  ].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
- }
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];
1081
1106
  }
1082
1107
  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
- }
1108
+ const line = prosePresentation(mark).className;
1109
+ return line === null ? void 0 : `${PROSE_BASE_CLASS} ${line}`;
1095
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";
1096
1116
  function ensureAssistantStyles() {
1097
1117
  if (typeof document === "undefined") return;
1098
1118
  const selector = `style[data-plugin-css=${JSON.stringify(STYLE_TAG_ID)}]`;
@@ -1163,7 +1183,7 @@ var ChatTranslateStore = class {
1163
1183
  }
1164
1184
  /**
1165
1185
  * 确保该行按当前文本被翻译。
1166
- * @param manual - 用户点击失败块发起的补跑:已落定的同代行只有这条路会重发。
1186
+ * @param manual - 用户点击失败块的左缘热区发起的补跑:已落定的同代行只有这条路会重发。
1167
1187
  * @returns 是否发出了新的请求。
1168
1188
  */
1169
1189
  ensure(rowKey, texts, manual = false) {
@@ -1425,13 +1445,6 @@ var ReasoningRow = (0, import_react4.memo)(function ReasoningRow2({
1425
1445
  )
1426
1446
  );
1427
1447
  });
1428
- function blockClick(event, action) {
1429
- const guardEvent = {
1430
- target: event.target,
1431
- currentTarget: event.currentTarget
1432
- };
1433
- if (isBareBlockClick(guardEvent, typeof window === "undefined" ? void 0 : window)) action();
1434
- }
1435
1448
  function ProseBlock({
1436
1449
  text,
1437
1450
  translated,
@@ -1439,41 +1452,62 @@ function ProseBlock({
1439
1452
  fail,
1440
1453
  onToggle,
1441
1454
  onRetry,
1455
+ canTranslate,
1442
1456
  streaming,
1443
1457
  labels,
1444
1458
  mentions,
1445
1459
  pathImages
1446
1460
  }) {
1447
- const action = proseAction(mark);
1461
+ const { action } = prosePresentation(mark);
1448
1462
  const clickable = action !== null;
1449
1463
  const retrying = action === "retry";
1450
1464
  const handler = retrying ? onRetry : onToggle;
1451
1465
  const source = mark === "translated" ? translated : text;
1452
1466
  const copy = rowCopy();
1453
- const block = React.createElement(
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
+ });
1503
+ return React.createElement(
1454
1504
  "div",
1455
1505
  {
1456
- className: proseClassNames(mark),
1457
- "data-translated": mark === "translated" ? "true" : void 0,
1458
- // 容器内含链接等交互内容,不套 role=button(非法嵌套);可聚焦 + 键盘
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) => {
1464
- if (event.target !== event.currentTarget) return;
1465
- if (event.key === "Enter" || event.key === " ") {
1466
- event.preventDefault();
1467
- handler();
1468
- }
1469
- } : void 0
1506
+ className,
1507
+ "data-translated": mark === "translated" ? "true" : void 0
1470
1508
  },
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),
1509
+ retryIcon,
1510
+ anchor,
1477
1511
  React.createElement(import_dsh_client_ui_primitives3.MarkdownText, {
1478
1512
  text: source,
1479
1513
  streaming,
@@ -1482,13 +1516,6 @@ function ProseBlock({
1482
1516
  pathImages
1483
1517
  })
1484
1518
  );
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
- });
1492
1519
  }
1493
1520
  var AssistantStepView = (0, import_react4.memo)(function AssistantStepView2(props) {
1494
1521
  ensureAssistantStyles();
@@ -1578,6 +1605,7 @@ var AssistantStepView = (0, import_react4.memo)(function AssistantStepView2(prop
1578
1605
  fail: entry.fail,
1579
1606
  onToggle: () => toggleBlock(entry.key),
1580
1607
  onRetry: retryRow,
1608
+ canTranslate,
1581
1609
  streaming,
1582
1610
  labels,
1583
1611
  mentions,