@lynn123411/dsh-chat-translate 3.1.0 → 3.3.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
@@ -50,7 +50,7 @@ __export(index_exports, {
50
50
  module.exports = __toCommonJS(index_exports);
51
51
 
52
52
  // src/client/settings/ui.tsx
53
- var import_react2 = require("react");
53
+ var import_react = require("react");
54
54
  var import_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
55
55
 
56
56
  // src/describe-error.ts
@@ -313,7 +313,7 @@ var SETTINGS_CSS = String.raw`
313
313
  display: flex;
314
314
  flex-direction: column;
315
315
  gap: 16px;
316
- max-width: 580px;
316
+ width: 100%;
317
317
  padding-bottom: 32px;
318
318
  color: var(--dsw-alias-label-primary);
319
319
  font: var(--dsw-font-s-14);
@@ -338,7 +338,14 @@ var SETTINGS_CSS = String.raw`
338
338
  font: var(--dsw-font-s-strong-14);
339
339
  }
340
340
 
341
+ /*
342
+ * Prose keeps a reading measure even though the card spans the host column: the
343
+ * master description is a 500-character paragraph, and at full width it would
344
+ * run to ~76 CJK characters per line. 46em tracks the element's own 12px font,
345
+ * so the measure survives a font-size change.
346
+ */
341
347
  .dsh-tidy-desc {
348
+ max-width: 46em;
342
349
  color: var(--dsw-alias-label-secondary);
343
350
  font: var(--dsw-font-xxs-12);
344
351
  }
@@ -404,147 +411,12 @@ var SETTINGS_CSS = String.raw`
404
411
 
405
412
  `;
406
413
 
407
- // src/client/reading-settings-page.tsx
408
- var import_react = require("react");
409
- var READING_PAGE_ID = "reading";
410
- var READING_PAGE_ORDER = 110;
411
- var READING_ITEM_SLOT = "reading.settings.item";
412
- function readLabel(label) {
413
- if (typeof label === "function") return label();
414
- return typeof label === "string" ? label : "";
415
- }
416
- var TABLIST_STYLE = {
417
- display: "flex",
418
- alignItems: "flex-end",
419
- gap: "22px",
420
- marginTop: "2px",
421
- marginBottom: "16px"
422
- };
423
- var TAB_STYLE = {
424
- appearance: "none",
425
- background: "transparent",
426
- // No border on ANY tab: the marker below is the active tab's own element, so
427
- // an inactive tab has nothing that could render a line.
428
- border: "none",
429
- position: "relative",
430
- padding: "7px 1px 11px",
431
- cursor: "pointer",
432
- font: "var(--dsw-font-xs-13)",
433
- color: "var(--dsw-alias-label-tertiary)"
434
- };
435
- var TAB_ACTIVE_STYLE = {
436
- ...TAB_STYLE,
437
- color: "var(--dsw-alias-label-primary)"
438
- };
439
- var TAB_MARKER_STYLE = {
440
- position: "absolute",
441
- left: 0,
442
- right: 0,
443
- bottom: 0,
444
- height: "2px",
445
- borderRadius: "2px 2px 0 0",
446
- background: "var(--dsw-alias-label-primary)"
447
- };
448
- var PANEL_STYLE = { listStyle: "none", margin: 0, padding: 0 };
449
- var PANEL_HIDDEN_STYLE = { ...PANEL_STYLE, display: "none" };
450
- function createReadingTabs(ctx) {
451
- const locale = ctx.get("locale");
452
- let version = -1;
453
- let revision = -1;
454
- let tabs = [];
455
- return {
456
- getSnapshot: () => {
457
- const nextVersion = ctx.slots.getVersion(READING_ITEM_SLOT);
458
- const nextRevision = locale === void 0 ? 0 : locale.getSnapshot().revision;
459
- if (nextVersion === version && nextRevision === revision) return tabs;
460
- version = nextVersion;
461
- revision = nextRevision;
462
- tabs = ctx.slots.entries(READING_ITEM_SLOT).map((entry) => ({
463
- id: entry.options.id ?? "",
464
- order: entry.options.order ?? 0,
465
- label: readLabel(entry.options.label)
466
- })).sort((left, right) => left.order - right.order);
467
- return tabs;
468
- },
469
- subscribe: (listener) => {
470
- const offSlots = ctx.slots.subscribe(READING_ITEM_SLOT, listener);
471
- const offLocale = locale?.subscribe(listener);
472
- return () => {
473
- offSlots();
474
- offLocale?.();
475
- };
476
- }
477
- };
478
- }
479
- function ReadingSettingsSection({ renderSlot, readingTabs }) {
480
- const tabs = (0, import_react.useSyncExternalStore)(
481
- readingTabs.subscribe,
482
- readingTabs.getSnapshot,
483
- readingTabs.getSnapshot
484
- );
485
- const [requested, setRequested] = (0, import_react.useState)(null);
486
- const selected = requested !== null && tabs.some((tab) => tab.id === requested) ? requested : tabs[0]?.id ?? null;
487
- if (selected === null) return null;
488
- return (0, import_react.createElement)(
489
- "div",
490
- null,
491
- (0, import_react.createElement)(
492
- "div",
493
- { role: "tablist", style: TABLIST_STYLE },
494
- tabs.map((tab) => (0, import_react.createElement)(
495
- "button",
496
- {
497
- key: tab.id,
498
- type: "button",
499
- role: "tab",
500
- "aria-selected": tab.id === selected,
501
- style: tab.id === selected ? TAB_ACTIVE_STYLE : TAB_STYLE,
502
- onClick: () => {
503
- setRequested(tab.id);
504
- }
505
- },
506
- tab.label,
507
- tab.id === selected ? (0, import_react.createElement)("span", { style: TAB_MARKER_STYLE, "aria-hidden": true }) : null
508
- ))
509
- ),
510
- tabs.map((tab) => (0, import_react.createElement)(
511
- "ul",
512
- {
513
- key: tab.id,
514
- role: "tabpanel",
515
- hidden: tab.id !== selected,
516
- style: tab.id === selected ? PANEL_STYLE : PANEL_HIDDEN_STYLE
517
- },
518
- renderSlot(READING_ITEM_SLOT, {}, { only: tab.id })
519
- ))
520
- );
521
- }
522
- function readingPageClaimed(ctx) {
523
- return ctx.slots.entries("settings.section").some((entry) => entry.options.id === READING_PAGE_ID);
524
- }
525
- function claimReadingSettingsPage(ctx, label, locale) {
526
- if (readingPageClaimed(ctx)) return () => {
527
- };
528
- const readingTabs = createReadingTabs(ctx);
529
- return ctx.slots.register({
530
- name: "settings.section",
531
- id: READING_PAGE_ID,
532
- order: READING_PAGE_ORDER,
533
- label,
534
- locale,
535
- inject: () => ({ readingTabs }),
536
- children: { "reading.settings.item": { kind: "list", scope: "root" } }
537
- }, ReadingSettingsSection);
538
- }
539
-
540
414
  // src/client/locales.ts
541
415
  var NS = "settings.chatTranslate";
542
416
  var zh = {
543
- pageNav: "\u9605\u8BFB\u4F53\u9A8C",
544
- title: "\u6B63\u6587\u7FFB\u8BD1",
545
417
  masterTitle: "\u7FFB\u8BD1\u603B\u5F00\u5173",
546
418
  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",
419
+ 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
420
  aiTitle: "AI \u7FFB\u8BD1\uFF08OpenAI \u517C\u5BB9\u534F\u8BAE\uFF09",
549
421
  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
422
  badgeConfigured: "\u5DF2\u914D\u7F6E",
@@ -574,14 +446,15 @@ var zh = {
574
446
  dockOn: "\u6B63\u6587\u7FFB\u8BD1\u5DF2\u5F00\u542F\uFF0C\u70B9\u51FB\u5173\u95ED",
575
447
  dockOff: "\u6B63\u6587\u7FFB\u8BD1\u5DF2\u5173\u95ED\uFF0C\u70B9\u51FB\u5F00\u542F",
576
448
  failTitle: "\u7FFB\u8BD1\u5931\u8D25",
577
- retryAria: "\u7FFB\u8BD1\u5931\u8D25\uFF0C\u6309 Enter \u91CD\u8BD5"
449
+ retryAria: "\u7FFB\u8BD1\u5931\u8D25\uFF0C\u70B9\u5DE6\u7F18\u90A3\u6761\u7A84\u5E26\u53EF\u6309 Enter \u91CD\u8BD5",
450
+ toggleToOriginalAria: "\u67E5\u770B\u539F\u6587",
451
+ toggleToTranslatedAria: "\u67E5\u770B\u8BD1\u6587",
452
+ inflightAria: "\u7FFB\u8BD1\u4E2D\uFF0C\u6B64\u5904\u6682\u65E0\u52A8\u4F5C"
578
453
  };
579
454
  var en = {
580
- pageNav: "Reading",
581
- title: "Reply translate",
582
455
  masterTitle: "Translation master switch",
583
456
  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.",
457
+ 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
458
  aiTitle: "AI translation (OpenAI-compatible)",
586
459
  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
460
  badgeConfigured: "Configured",
@@ -611,7 +484,10 @@ var en = {
611
484
  dockOn: "Translation is on; click to turn it off",
612
485
  dockOff: "Translation is off; click to turn it on",
613
486
  failTitle: "Translation failed",
614
- retryAria: "Translation failed \u2014 press Enter to retry"
487
+ retryAria: "Translation failed \u2014 focus the slim strip at the left edge and press Enter to retry",
488
+ toggleToOriginalAria: "Show the original",
489
+ toggleToTranslatedAria: "Show the translation",
490
+ inflightAria: "Translating; Enter does nothing yet"
615
491
  };
616
492
  var rowT = (key) => zh[key] ?? key;
617
493
  function bindRowCopy(locale) {
@@ -623,6 +499,9 @@ function bindRowCopy(locale) {
623
499
  function rowCopy() {
624
500
  return {
625
501
  retryAria: rowT("retryAria"),
502
+ toggleToOriginalAria: rowT("toggleToOriginalAria"),
503
+ toggleToTranslatedAria: rowT("toggleToTranslatedAria"),
504
+ inflightAria: rowT("inflightAria"),
626
505
  failTitle: (detail) => {
627
506
  const label = rowT("failTitle");
628
507
  return detail === void 0 || detail === "" || detail === null ? label : `${label} \u2014 ${detail}`;
@@ -630,6 +509,16 @@ function rowCopy() {
630
509
  };
631
510
  }
632
511
 
512
+ // src/client/settings/plugin-page.ts
513
+ var BUNDLE_NAME = "@lynn123411/dsh-chat-translate";
514
+ var BUNDLE_CONFIG_SLOT = "plugins.bundle.config";
515
+ var BUNDLE_CONFIG_KEY = BUNDLE_NAME;
516
+ function setupPluginPage(slots, register) {
517
+ if (!slots || typeof slots.inject !== "function") return () => {
518
+ };
519
+ return slots.inject(BUNDLE_CONFIG_SLOT, register);
520
+ }
521
+
633
522
  // src/client/settings/ui.tsx
634
523
  var import_jsx_runtime = require("react/jsx-runtime");
635
524
  var stylesInjected = false;
@@ -650,14 +539,14 @@ function withSamples(text, samples) {
650
539
  });
651
540
  }
652
541
  function TidySettingsPanel(props) {
653
- ensureSettingsStyles();
654
542
  const { t } = props;
655
- const [state, setState] = (0, import_react2.useState)(() => settingsStore.getState());
656
- const [testing, setTesting] = (0, import_react2.useState)(null);
657
- const [apiKeyInput, setApiKeyInput] = (0, import_react2.useState)("");
658
- const [savingKey, setSavingKey] = (0, import_react2.useState)(false);
659
- const [keyMsg, setKeyMsg] = (0, import_react2.useState)(null);
660
- (0, import_react2.useEffect)(() => {
543
+ ensureSettingsStyles();
544
+ const [state, setState] = (0, import_react.useState)(() => settingsStore.getState());
545
+ const [testing, setTesting] = (0, import_react.useState)(null);
546
+ const [apiKeyInput, setApiKeyInput] = (0, import_react.useState)("");
547
+ const [savingKey, setSavingKey] = (0, import_react.useState)(false);
548
+ const [keyMsg, setKeyMsg] = (0, import_react.useState)(null);
549
+ (0, import_react.useEffect)(() => {
661
550
  return settingsStore.subscribe(() => {
662
551
  setState(settingsStore.getState());
663
552
  });
@@ -844,52 +733,34 @@ function setupSettingsUi(ctx) {
844
733
  "dsh-chat-translate: locale dictionaries"
845
734
  );
846
735
  }
847
- const t = locale && typeof locale.bind === "function" ? locale.bind(NS) : (key) => zh[key] ?? key;
848
- try {
849
- const slots = ctx.slots;
850
- if (!slots || typeof slots.inject !== "function") return;
851
- ctx.effect(
852
- () => configForms.whileServed([SETTINGS_NAMESPACE], () => {
853
- const offPage = slots.inject(
854
- "settings.section",
855
- () => claimReadingSettingsPage(ctx, () => t("pageNav"), NS)
856
- );
857
- const offCard = slots.inject(
858
- READING_ITEM_SLOT,
859
- () => slots.register(
860
- {
861
- name: READING_ITEM_SLOT,
862
- // id = 本插件的配置条目 id(= profile entry id = 设置命名空间)
863
- id: SETTINGS_NAMESPACE,
864
- // 卡片在共享页里的顺序:丝滑流式 10、吸顶提示 20、聊天翻译 30
865
- order: 30,
866
- // 共享页按此标签渲染 tab
867
- label: () => t("title"),
868
- locale: NS
869
- },
870
- TidySettingsPanel
871
- )
872
- );
873
- return () => {
874
- offCard();
875
- offPage();
876
- };
877
- }),
878
- "dsh-chat-translate: shared reading page"
879
- );
880
- } catch (err) {
881
- console.warn("[dsh-chat-translate] Failed to inject settings section:", err);
882
- }
736
+ ctx.effect(
737
+ () => configForms.whileServed([SETTINGS_NAMESPACE], () => setupPluginPage(
738
+ ctx.slots,
739
+ () => ctx.slots.register(
740
+ {
741
+ name: BUNDLE_CONFIG_SLOT,
742
+ // The bundle's package name: the key that puts this entry on this
743
+ // bundle's own page, above its rows.
744
+ key: BUNDLE_CONFIG_KEY,
745
+ // The page draws the title, icon and crumb itself; the card keeps its
746
+ // own chrome, controls and section order.
747
+ locale: NS
748
+ },
749
+ TidySettingsPanel
750
+ )
751
+ )),
752
+ "dsh-chat-translate: plugins page config"
753
+ );
883
754
  }
884
755
 
885
756
  // src/client/composer/dock-toggle.tsx
886
- var import_react3 = require("react");
757
+ var import_react2 = require("react");
887
758
  var import_dsh_client_ui_primitives2 = require("@deepseek-ai/dsh-client-ui-primitives");
888
759
  var import_jsx_runtime2 = require("react/jsx-runtime");
889
760
  function ComposerTranslateToggle(props) {
890
761
  const { t } = props;
891
- const [state, setState] = (0, import_react3.useState)(() => settingsStore.getState());
892
- (0, import_react3.useEffect)(() => settingsStore.subscribe(() => setState(settingsStore.getState())), []);
762
+ const [state, setState] = (0, import_react2.useState)(() => settingsStore.getState());
763
+ (0, import_react2.useEffect)(() => settingsStore.subscribe(() => setState(settingsStore.getState())), []);
893
764
  return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(import_dsh_client_ui_primitives2.Tooltip, { label: () => state.enabled ? t("dockOn") : t("dockOff"), side: "top", align: "end", portal: true, children: /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
894
765
  import_dsh_client_ui_primitives2.Pill,
895
766
  {
@@ -922,7 +793,7 @@ function setupComposerToggle(ctx) {
922
793
 
923
794
  // src/client/chat/assistant-step.tsx
924
795
  var React = __toESM(require("react"), 1);
925
- var import_react4 = require("react");
796
+ var import_react3 = require("react");
926
797
  var import_dsh_client_ui_primitives3 = require("@deepseek-ai/dsh-client-ui-primitives");
927
798
 
928
799
  // src/client/chat/row-plan.ts
@@ -1005,10 +876,8 @@ function planAssistantRow(input) {
1005
876
  }
1006
877
 
1007
878
  // 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;
879
+ function isCleanHotspotClick(event, view) {
880
+ if (event.target !== event.currentTarget) return false;
1012
881
  const selection = typeof view?.getSelection === "function" ? view.getSelection() : null;
1013
882
  if (selection !== null && selection !== void 0) {
1014
883
  if (selection.isCollapsed === false && String(selection.toString?.() ?? "") !== "") return false;
@@ -1028,25 +897,42 @@ var ASSISTANT_CSS = [
1028
897
  ".dsh-ct-body .md-table-wide>table{z-index:1;position:relative}",
1029
898
  // ---- 已停止标记(宿主 .hWmORq_stopped 等价) ----
1030
899
  ".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)}",
900
+ // ---- 正文块:可翻译时左缘留出 12px 缩进(缩进与线无关——译文出现前后
901
+ // 正文位置不变,切换总开关时整列也不横移),并成为热区的定位父级 ----
902
+ ".dsh-ct-prose{position:relative}",
903
+ ".dsh-ct-prose-indent{padding-left:12px}",
904
+ // ---- 线型:粗细与色相只表示状态。蓝粗线=读译文,灰细线=读原文备着译文
905
+ // (0.5px 中性 hairline),红实线=失败(只剩通道伤一种) ----
1035
906
  ".dsh-ct-prose-translated{border-left:1px solid color-mix(in srgb, var(--dsw-alias-state-business-primary) 65%, transparent)}",
1036
907
  ".dsh-ct-prose-original{border-left:0.5px solid var(--dsw-alias-border-l2)}",
1037
- // ---- 失败标记:1px error 色红实线(只剩通道伤一种,悬停报细节);
1038
- // 可点,点=整行手动补跑 ----
1039
908
  ".dsh-ct-prose-failed{border-left:1px solid color-mix(in srgb, var(--dsw-alias-state-error-primary) 65%, transparent)}",
1040
- // ---- 在途:灰 1px 线脉动(与成功态同 padding,转蓝不跳字);
909
+ // ---- 在途:灰 1px 线脉动(缩进由 .dsh-ct-prose-indent 统一给,转蓝不跳字);
1041
910
  // 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}",
911
+ ".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
912
  "@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
913
  "@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}",
914
+ // ---- 左缘热区:随块高铺满的一条透明窄带。绝对定位的偏移以 padding box 为
915
+ // 基准、而线画在 border 上,所以线的左缘在 -1px:`left:-5px` 给出线外侧
916
+ // 4px,`width:17px` 让右缘落在正文第一个字上(12px 缩进),不吃正文。
917
+ // 切换与补跑只由它承载;正文整块不再是点击目标,选中/复制不再误触。
918
+ // z-index 压过宽表格的透明出血层(那条规则给 table 挂了 z-index:1);
919
+ // 出血区是透明 padding,正文格子的起点仍在缩进之外 ----
920
+ ".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}",
921
+ // 在途只剩「已在跑」这一答复:热区保留(键盘焦点不掉),但光标不承诺能点。
922
+ ".dsh-ct-hotspot[data-idle]{cursor:default}",
923
+ // 热区没有自己的外形:它是透明的一层。focus-visible 只对键盘聚焦成立(指针
924
+ // 点出来的焦点不匹配),下面的 :has 规则把线加亮——粗细始终只表示译文/原文。
925
+ ".dsh-ct-hotspot:focus-visible{outline:none}",
926
+ ".dsh-ct-prose-translated:has(.dsh-ct-hotspot:focus-visible){border-left-color:var(--dsw-alias-state-business-primary)}",
927
+ ".dsh-ct-prose-original:has(.dsh-ct-hotspot:focus-visible){border-left-color:var(--dsw-alias-label-caption)}",
928
+ ".dsh-ct-prose-failed:has(.dsh-ct-hotspot:focus-visible){border-left-color:var(--dsw-alias-state-error-primary)}",
929
+ // 在途线本就在动;CSS 动画压过普通声明,聚焦的加亮必须同时停掉动画才看得见
930
+ // (停掉脉动本身就是一次可见的变化,线也从闪烁的灰换成静止的亮灰)。
931
+ ".dsh-ct-prose-inflight:has(.dsh-ct-hotspot:focus-visible){animation:none;border-left-color:var(--dsw-alias-label-caption)}",
932
+ // ---- 失败块的补跑指引:小 ↻(官方图标)常驻在红线旁——失败不会被悬停
933
+ // 发现,救活入口必须一直看得见;纯装饰(pointer-events:none),点击
934
+ // 归热区,败因由 Tooltip 挂在热区上 ----
935
+ ".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
936
  // ---- ReasoningRow(宿主 lcKema_* 等价,前缀换成 dsh-ct-think) ----
1051
937
  ".dsh-ct-think-root{flex-direction:column;display:flex}",
1052
938
  ".dsh-ct-think-root:not([data-expanded]){contain:size layout;height:calc(24px + var(--dsh-content-font-delta,0px))}",
@@ -1068,31 +954,27 @@ var ASSISTANT_CSS = [
1068
954
  // ---- 无障碍:屏幕阅读器专用文本 ----
1069
955
  ".dsh-ct-visually-hidden{clip:rect(0 0 0 0);white-space:nowrap;width:1px;height:1px;position:absolute;overflow:hidden}"
1070
956
  ].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
- }
957
+ var PROSE_BY_MARK = {
958
+ translated: { className: "dsh-ct-prose-translated", action: "toggle" },
959
+ "original-view": { className: "dsh-ct-prose-original", action: "toggle" },
960
+ failed: { className: "dsh-ct-prose-failed", action: "retry" },
961
+ // 在途有线、有热区,但 action 为 null:按下去是空操作(脉动已是「已在跑」
962
+ // 的答复),热区保留只是为了让键盘焦点不掉回页面。
963
+ inflight: { className: "dsh-ct-prose-inflight", action: null }
964
+ };
965
+ var PROSE_BASE_CLASS = "dsh-ct-prose";
966
+ function prosePresentation(mark) {
967
+ return mark === null ? { className: null, action: null } : PROSE_BY_MARK[mark];
1081
968
  }
1082
969
  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
- }
970
+ const line = prosePresentation(mark).className;
971
+ return line === null ? void 0 : `${PROSE_BASE_CLASS} ${line}`;
1095
972
  }
973
+ function proseIndentClassName(canTranslate) {
974
+ return canTranslate ? "dsh-ct-prose-indent" : void 0;
975
+ }
976
+ var HOTSPOT_CLASS = "dsh-ct-hotspot";
977
+ var HOTSPOT_IDLE_ATTR = "data-idle";
1096
978
  function ensureAssistantStyles() {
1097
979
  if (typeof document === "undefined") return;
1098
980
  const selector = `style[data-plugin-css=${JSON.stringify(STYLE_TAG_ID)}]`;
@@ -1163,7 +1045,7 @@ var ChatTranslateStore = class {
1163
1045
  }
1164
1046
  /**
1165
1047
  * 确保该行按当前文本被翻译。
1166
- * @param manual - 用户点击失败块发起的补跑:已落定的同代行只有这条路会重发。
1048
+ * @param manual - 用户点击失败块的左缘热区发起的补跑:已落定的同代行只有这条路会重发。
1167
1049
  * @returns 是否发出了新的请求。
1168
1050
  */
1169
1051
  ensure(rowKey, texts, manual = false) {
@@ -1343,8 +1225,8 @@ var subscribeSettings = (listener) => settingsStore.subscribe(listener);
1343
1225
  var subscribeTranslate = (listener) => chatTranslate.subscribe(listener);
1344
1226
  function useSettledInView(ref, armed) {
1345
1227
  const noObserver = typeof IntersectionObserver === "undefined";
1346
- const [inView, setInView] = (0, import_react4.useState)(noObserver);
1347
- (0, import_react4.useEffect)(() => {
1228
+ const [inView, setInView] = (0, import_react3.useState)(noObserver);
1229
+ (0, import_react3.useEffect)(() => {
1348
1230
  if (noObserver || !armed) return;
1349
1231
  const element = ref.current;
1350
1232
  if (element === null) return;
@@ -1361,7 +1243,7 @@ function useSettledInView(ref, armed) {
1361
1243
  }, [armed, noObserver, ref]);
1362
1244
  return inView;
1363
1245
  }
1364
- var ReasoningRow = (0, import_react4.memo)(function ReasoningRow2({
1246
+ var ReasoningRow = (0, import_react3.memo)(function ReasoningRow2({
1365
1247
  text,
1366
1248
  running,
1367
1249
  usePresentation,
@@ -1369,13 +1251,13 @@ var ReasoningRow = (0, import_react4.memo)(function ReasoningRow2({
1369
1251
  t
1370
1252
  }) {
1371
1253
  const { expanded, toggle } = useDisclosure();
1372
- const labels = (0, import_react4.useMemo)(() => markdownLabels(t), [t]);
1254
+ const labels = (0, import_react3.useMemo)(() => markdownLabels(t), [t]);
1373
1255
  const summaryText = running ? latestCompletedParagraphFirstLine(text) : firstLine(text);
1374
- const summary = (0, import_react4.useMemo)(() => summaryText.replaceAll("**", ""), [summaryText]);
1256
+ const summary = (0, import_react3.useMemo)(() => summaryText.replaceAll("**", ""), [summaryText]);
1375
1257
  const preview = usePresentation(
1376
1258
  (policy2) => !expanded && summary !== "" && (running || policy2.settledReasoningPreview)
1377
1259
  );
1378
- const collapsedContent = (0, import_react4.useMemo)(
1260
+ const collapsedContent = (0, import_react3.useMemo)(
1379
1261
  () => React.createElement(
1380
1262
  React.Fragment,
1381
1263
  null,
@@ -1388,7 +1270,7 @@ var ReasoningRow = (0, import_react4.memo)(function ReasoningRow2({
1388
1270
  ),
1389
1271
  [running, summary]
1390
1272
  );
1391
- const content = (0, import_react4.useMemo)(
1273
+ const content = (0, import_react3.useMemo)(
1392
1274
  () => expanded ? React.createElement(
1393
1275
  "div",
1394
1276
  { className: "dsh-ct-think-body" },
@@ -1425,13 +1307,6 @@ var ReasoningRow = (0, import_react4.memo)(function ReasoningRow2({
1425
1307
  )
1426
1308
  );
1427
1309
  });
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
1310
  function ProseBlock({
1436
1311
  text,
1437
1312
  translated,
@@ -1439,41 +1314,62 @@ function ProseBlock({
1439
1314
  fail,
1440
1315
  onToggle,
1441
1316
  onRetry,
1317
+ canTranslate,
1442
1318
  streaming,
1443
1319
  labels,
1444
1320
  mentions,
1445
1321
  pathImages
1446
1322
  }) {
1447
- const action = proseAction(mark);
1323
+ const { action } = prosePresentation(mark);
1448
1324
  const clickable = action !== null;
1449
1325
  const retrying = action === "retry";
1450
1326
  const handler = retrying ? onRetry : onToggle;
1451
1327
  const source = mark === "translated" ? translated : text;
1452
1328
  const copy = rowCopy();
1453
- const block = React.createElement(
1329
+ const className = [proseClassNames(mark) ?? PROSE_BASE_CLASS, proseIndentClassName(canTranslate)].filter((name2) => name2 !== void 0).join(" ");
1330
+ const hotspot = mark === null ? null : React.createElement("button", {
1331
+ type: "button",
1332
+ className: HOTSPOT_CLASS,
1333
+ // 热区是覆盖层,读屏只应听到它的动作,正文仍由 MarkdownText 读。
1334
+ "aria-label": mark === "inflight" ? copy.inflightAria : retrying ? copy.retryAria : mark === "original-view" ? copy.toggleToTranslatedAria : copy.toggleToOriginalAria,
1335
+ ...mark === "inflight" ? { [HOTSPOT_IDLE_ATTR]: "true" } : {},
1336
+ tabIndex: 0,
1337
+ // button 自己会按 Enter/Space 合成 click,若不接管就会与这段键盘处理
1338
+ // 叠加成两次动作:这里两个键都走 keydown 并 preventDefault,合成被掐掉。
1339
+ onKeyDown: clickable ? (event) => {
1340
+ if (event.key !== "Enter" && event.key !== " ") return;
1341
+ event.preventDefault();
1342
+ handler();
1343
+ } : void 0,
1344
+ onClick: clickable ? (event) => {
1345
+ if (isCleanHotspotClick(
1346
+ { target: event.target, currentTarget: event.currentTarget },
1347
+ typeof window === "undefined" ? void 0 : window
1348
+ )) {
1349
+ handler();
1350
+ }
1351
+ } : void 0
1352
+ });
1353
+ const retryIcon = retrying ? React.createElement(
1354
+ "span",
1355
+ { className: "dsh-ct-retry", "aria-hidden": true },
1356
+ React.createElement(import_dsh_client_ui_primitives3.IconRefreshOutlineRegular, { size: 10 })
1357
+ ) : null;
1358
+ const anchor = hotspot === null ? null : React.createElement(import_dsh_client_ui_primitives3.Tooltip, {
1359
+ label: () => copy.failTitle(fail),
1360
+ side: "top",
1361
+ portal: true,
1362
+ disabled: !retrying,
1363
+ children: hotspot
1364
+ });
1365
+ return React.createElement(
1454
1366
  "div",
1455
1367
  {
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
1368
+ className,
1369
+ "data-translated": mark === "translated" ? "true" : void 0
1470
1370
  },
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),
1371
+ retryIcon,
1372
+ anchor,
1477
1373
  React.createElement(import_dsh_client_ui_primitives3.MarkdownText, {
1478
1374
  text: source,
1479
1375
  streaming,
@@ -1482,40 +1378,33 @@ function ProseBlock({
1482
1378
  pathImages
1483
1379
  })
1484
1380
  );
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
1381
  }
1493
- var AssistantStepView = (0, import_react4.memo)(function AssistantStepView2(props) {
1382
+ var AssistantStepView = (0, import_react3.memo)(function AssistantStepView2(props) {
1494
1383
  ensureAssistantStyles();
1495
1384
  const { node, groupPart, useDisclosure, useTurnData, turnProcess, openFile, renderMessageImages, fileMentions, usePresentation, t } = props;
1496
1385
  const data = node.data;
1497
1386
  const streaming = data.status === "running";
1498
1387
  const interrupted = data.status === "interrupted";
1499
- const enabled = (0, import_react4.useSyncExternalStore)(subscribeSettings, () => settingsStore.getState().enabled);
1500
- const aiConfigured = (0, import_react4.useSyncExternalStore)(subscribeSettings, () => settingsStore.getState().aiConfigured);
1501
- (0, import_react4.useSyncExternalStore)(subscribeTranslate, () => chatTranslate.getVersion());
1388
+ const enabled = (0, import_react3.useSyncExternalStore)(subscribeSettings, () => settingsStore.getState().enabled);
1389
+ const aiConfigured = (0, import_react3.useSyncExternalStore)(subscribeSettings, () => settingsStore.getState().aiConfigured);
1390
+ (0, import_react3.useSyncExternalStore)(subscribeTranslate, () => chatTranslate.getVersion());
1502
1391
  const canTranslate = enabled && aiConfigured;
1503
1392
  const rowKey = `a${node.anchorSeq ?? `t${data.turn}:s${data.step}`}`;
1504
- const labels = (0, import_react4.useMemo)(() => markdownLabels(t), [t]);
1505
- const pathImages = (0, import_react4.useMemo)(
1393
+ const labels = (0, import_react3.useMemo)(() => markdownLabels(t), [t]);
1394
+ const pathImages = (0, import_react3.useMemo)(
1506
1395
  () => ({ resolve: (value) => localPathMediaUrl(document.baseURI, value) }),
1507
1396
  []
1508
1397
  );
1509
1398
  const turn = node.location?.kind === "turn" || node.location?.kind === "step" ? node.location.turn : void 0;
1510
1399
  const tail = useTurnData("turn-tail");
1511
- const owner = (0, import_react4.useMemo)(() => {
1400
+ const owner = (0, import_react3.useMemo)(() => {
1512
1401
  if (turn?.status !== "closed" || data.finalNode === void 0) return void 0;
1513
1402
  if (tail?.closing?.finalNode?.seq !== data.finalNode.seq) return void 0;
1514
1403
  return { turn, seq: data.finalNode.seq, openFile };
1515
1404
  }, [data.finalNode, openFile, tail, turn]);
1516
- const mentions = (0, import_react4.useMemo)(() => owner === void 0 ? void 0 : fileMentions(owner), [fileMentions, owner]);
1517
- const blocks = (0, import_react4.useMemo)(() => data.blocks ?? [], [data.blocks]);
1518
- const plan = (0, import_react4.useMemo)(
1405
+ const mentions = (0, import_react3.useMemo)(() => owner === void 0 ? void 0 : fileMentions(owner), [fileMentions, owner]);
1406
+ const blocks = (0, import_react3.useMemo)(() => data.blocks ?? [], [data.blocks]);
1407
+ const plan = (0, import_react3.useMemo)(
1519
1408
  () => planAssistantRow({
1520
1409
  blocks,
1521
1410
  streaming,
@@ -1526,16 +1415,16 @@ var AssistantStepView = (0, import_react4.memo)(function AssistantStepView2(prop
1526
1415
  }),
1527
1416
  [blocks, streaming, interrupted, groupPart, canTranslate]
1528
1417
  );
1529
- const rootRef = (0, import_react4.useRef)(null);
1418
+ const rootRef = (0, import_react3.useRef)(null);
1530
1419
  const inView = useSettledInView(rootRef, !streaming && canTranslate && plan.entries !== null && plan.texts.length > 0);
1531
- (0, import_react4.useEffect)(() => {
1420
+ (0, import_react3.useEffect)(() => {
1532
1421
  if (streaming || !canTranslate || !inView || plan.entries === null || plan.texts.length === 0) return;
1533
1422
  chatTranslate.ensure(rowKey, plan.texts);
1534
1423
  }, [streaming, canTranslate, inView, rowKey, plan]);
1535
- const [originalKeys, setOriginalKeys] = (0, import_react4.useState)(() => /* @__PURE__ */ new Set());
1424
+ const [originalKeys, setOriginalKeys] = (0, import_react3.useState)(() => /* @__PURE__ */ new Set());
1536
1425
  const registered = chatTranslate.getState(rowKey);
1537
1426
  const rowState = registered !== void 0 && sameTexts(registered.texts, plan.texts) ? registered : void 0;
1538
- const displayed = (0, import_react4.useMemo)(
1427
+ const displayed = (0, import_react3.useMemo)(
1539
1428
  () => planAssistantRow({
1540
1429
  blocks,
1541
1430
  streaming,
@@ -1549,10 +1438,10 @@ var AssistantStepView = (0, import_react4.memo)(function AssistantStepView2(prop
1549
1438
  [blocks, streaming, interrupted, groupPart, canTranslate, rowState, originalKeys]
1550
1439
  );
1551
1440
  const reasoningHidden = turnProcess !== void 0 && turnProcess.foldable && turnProcess.spec.answerStep === data.step && turnProcess.spec.inlineReasoning && !turnProcess.open;
1552
- const revealProcess = (0, import_react4.useCallback)(() => {
1441
+ const revealProcess = (0, import_react3.useCallback)(() => {
1553
1442
  turnProcess?.setOpen(true);
1554
1443
  }, [turnProcess]);
1555
- const toggleBlock = (0, import_react4.useCallback)((key) => {
1444
+ const toggleBlock = (0, import_react3.useCallback)((key) => {
1556
1445
  setOriginalKeys((current) => {
1557
1446
  const next = new Set(current);
1558
1447
  if (next.has(key)) next.delete(key);
@@ -1560,7 +1449,7 @@ var AssistantStepView = (0, import_react4.memo)(function AssistantStepView2(prop
1560
1449
  return next;
1561
1450
  });
1562
1451
  }, []);
1563
- const retryRow = (0, import_react4.useCallback)(() => {
1452
+ const retryRow = (0, import_react3.useCallback)(() => {
1564
1453
  chatTranslate.ensure(rowKey, plan.texts, true);
1565
1454
  }, [rowKey, plan]);
1566
1455
  if (displayed.entries === null) return null;
@@ -1578,6 +1467,7 @@ var AssistantStepView = (0, import_react4.memo)(function AssistantStepView2(prop
1578
1467
  fail: entry.fail,
1579
1468
  onToggle: () => toggleBlock(entry.key),
1580
1469
  onRetry: retryRow,
1470
+ canTranslate,
1581
1471
  streaming,
1582
1472
  labels,
1583
1473
  mentions,
@@ -1745,7 +1635,7 @@ function apply(ctx) {
1745
1635
  chatPresentation.attach(form);
1746
1636
  return () => chatPresentation.detach();
1747
1637
  }, "dsh-chat-translate: chat presentation source");
1748
- ctx.effect(() => setupSettingsUi(ctx), "dsh-chat-translate: settings section");
1638
+ ctx.effect(() => setupSettingsUi(ctx), "dsh-chat-translate: plugins page config");
1749
1639
  ctx.effect(() => setupComposerToggle(ctx), "dsh-chat-translate: composer dock toggle");
1750
1640
  bindRowCopy(ctx.locale);
1751
1641
  }