@xiaoso/dsh-tool-plus 0.1.0-beta.5 → 0.1.0-beta.7

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.
Files changed (71) hide show
  1. package/README.en.md +1 -1
  2. package/README.md +2 -2
  3. package/THIRD_PARTY_NOTICES.md +42 -42
  4. package/lib/{abort-dIkZBV7Q.mjs → abort-CsjZitYv.mjs} +1 -1
  5. package/lib/{amazon-bedrock-WQhvjHYu.mjs → amazon-bedrock-CfNIDi5k.mjs} +7 -7
  6. package/lib/{anthropic-CJCauvYz.mjs → anthropic-BVfZdnMp.mjs} +12 -12
  7. package/lib/{anthropic-tTntD8BD.mjs → anthropic-DGXmmg43.mjs} +2 -2
  8. package/lib/{api-key-login-BzqNdM2y.mjs → api-key-login-BikQAfD-.mjs} +1 -1
  9. package/lib/{aws-credentials-CXryMobh.mjs → aws-credentials-DrssNMn_.mjs} +4 -4
  10. package/lib/{azure-openai-responses-DuukbD0F.mjs → azure-openai-responses-C-5q-5K2.mjs} +8 -8
  11. package/lib/browser-render-z6b8Ljo9.mjs +183 -0
  12. package/lib/{bundled-references-Cavaffah.mjs → bundled-references-D71z7_T3.mjs} +2 -2
  13. package/lib/{callback-server-CQ5Jo8F-.mjs → callback-server-Di6C4RqI.mjs} +1 -1
  14. package/lib/client.js +613 -107
  15. package/lib/{cursor-B6TUEmgP.mjs → cursor-CQQtygwl.mjs} +1 -1
  16. package/lib/{cursor-DkSX2h3H.mjs → cursor-DXJxwdcg.mjs} +7 -7
  17. package/lib/{cursor-aJrXeYCb.mjs → cursor-Hr5Zw1gt.mjs} +3 -3
  18. package/lib/{device-code-DBo3B5nh.mjs → device-code-wZUbgPdB.mjs} +1 -1
  19. package/lib/{devin-BHZZyMhO.mjs → devin-CF6UhCFw.mjs} +2 -2
  20. package/lib/{devin-BK7DHeca.mjs → devin-DXJoDlW2.mjs} +1 -1
  21. package/lib/{devin-vNZTNUON.mjs → devin-P37gURs_.mjs} +5 -5
  22. package/lib/{dom-DGdYAVTv.mjs → dom-CawWi8fa.mjs} +14 -1
  23. package/lib/{error-Dd8DIqs-.mjs → error-DoHuatWw.mjs} +1 -1
  24. package/lib/{event-stream-BbFZJy-z.mjs → event-stream-C-FmBYUt.mjs} +1 -1
  25. package/lib/{github-copilot-CYkjeTi3.mjs → github-copilot-C2dTonBX.mjs} +3 -3
  26. package/lib/{github-copilot-B1yiyVRh.mjs → github-copilot-qETlj_bD.mjs} +1 -1
  27. package/lib/{gitlab-duo-CM7Ref1H.mjs → gitlab-duo-FQZiFRQe.mjs} +3 -3
  28. package/lib/{gitlab-duo-workflow-BGAF6JDw.mjs → gitlab-duo-workflow-DLTabzOy.mjs} +2 -2
  29. package/lib/{google-BS4T25Pn.mjs → google-BilwFSzx.mjs} +3 -3
  30. package/lib/{google-antigravity-DGmwC2KV.mjs → google-antigravity-BfBiINdu.mjs} +2 -2
  31. package/lib/{google-auth-B6gjsZvg.mjs → google-auth-CWlKXJ-H.mjs} +3 -3
  32. package/lib/{google-gemini-cli-Dsztq0N5.mjs → google-gemini-cli-CB89ljNz.mjs} +3 -3
  33. package/lib/{google-gemini-cli-Cqds_Q5T.mjs → google-gemini-cli-CygFcR6N.mjs} +8 -8
  34. package/lib/{google-oauth-shared-CuEHaDDj.mjs → google-oauth-shared-Cn82h1o2.mjs} +2 -2
  35. package/lib/{google-shared-Dr-VG-ap.mjs → google-shared-CF1JrqTM.mjs} +6 -6
  36. package/lib/{google-vertex-BDN2tLaD.mjs → google-vertex-DgMhhFw8.mjs} +5 -5
  37. package/lib/{identity-Da1GPU9a.mjs → identity-C0SPVyIZ.mjs} +1 -1
  38. package/lib/{idle-iterator-DUjKQEGT.mjs → idle-iterator-HzbbfXP4.mjs} +2 -2
  39. package/lib/index.mjs +310 -29
  40. package/lib/{kimi-Dh0pv3WZ.mjs → kimi-DHDO6Fde.mjs} +3 -3
  41. package/lib/launch-DwpE70Pv.mjs +148 -0
  42. package/lib/{markit-BL3cH47U.mjs → markit-B9exZ0ZV.mjs} +1 -1
  43. package/lib/{minimax-code-Uq8Qu9LW.mjs → minimax-code-vAhEXhev.mjs} +1 -1
  44. package/lib/{models-SGASygJ3.mjs → models-Cp3PzU3l.mjs} +1 -1
  45. package/lib/{ollama-DPzBmq89.mjs → ollama-TrkXTNqE.mjs} +8 -8
  46. package/lib/{openai-codex-Ch8CkT3p.mjs → openai-codex-CrjHtSH4.mjs} +3 -3
  47. package/lib/{openai-codex-responses-pejZgdZo.mjs → openai-codex-responses-C4uMpWNy.mjs} +10 -10
  48. package/lib/{openai-completions-DIf361kF.mjs → openai-completions-1ydXm5vF.mjs} +13 -13
  49. package/lib/{openai-reasoning-fallback-Nj6yB2rY.mjs → openai-reasoning-fallback-CigKCyzc.mjs} +2 -2
  50. package/lib/{openai-responses-Db_a-U4-.mjs → openai-responses-CN79gRGQ.mjs} +10 -10
  51. package/lib/{opencode-Rq_Q2vAO.mjs → opencode-CEGlimzL.mjs} +1 -1
  52. package/lib/{perplexity-C4iLVMUj.mjs → perplexity-BLJN-8dM.mjs} +2 -2
  53. package/lib/{proxy-pM_NIv-H.mjs → proxy-XOYi58TM.mjs} +1 -1
  54. package/lib/readability-Cpm-ocau.mjs +455 -0
  55. package/lib/{scrapers-1FTlHNQV.mjs → scrapers-DrSYgPyf.mjs} +11 -10
  56. package/lib/{src-BgpusnsB.mjs → src-D_e1qMwi.mjs} +5 -1
  57. package/lib/{stream-CRtexYLe.mjs → stream-DveNFWSN.mjs} +56 -56
  58. package/lib/{stream-markup-healing-KMkiAA00.mjs → stream-markup-healing-C9eC7Gf8.mjs} +3 -3
  59. package/lib/tool-errors-DoyxivAR.mjs +47 -0
  60. package/lib/types-BUO0JXO-.mjs +251 -0
  61. package/lib/{utils-D2YknRk4.mjs → utils-B-mTYQBc.mjs} +1 -1
  62. package/lib/{utils-C616y7eF.mjs → utils-DSEEKy3G.mjs} +28 -268
  63. package/lib/{utils-Cp76gCyk.mjs → utils-fnV2zzp3.mjs} +3 -3
  64. package/lib/{vision-guard-01qSwFpf.mjs → vision-guard-BsvOWmfB.mjs} +2 -2
  65. package/lib/{wafer-CgtPHerf.mjs → wafer-Dpy7tL3l.mjs} +1 -1
  66. package/lib/{xai-oauth-Buk8XVvM.mjs → xai-oauth-BkPNQS-q.mjs} +2 -2
  67. package/lib/{xiaomi-D2qT5cAC.mjs → xiaomi-CG3TsYRz.mjs} +1 -1
  68. package/lib/{zai-C59YnomG.mjs → zai--cewobzN.mjs} +2 -2
  69. package/lib/{zip-Cg9ANN_U.mjs → zip-CqKW2-TK.mjs} +3 -46
  70. package/package.json +3 -1
  71. package/presets/package.json +1 -1
package/lib/client.js CHANGED
@@ -7,6 +7,7 @@ window.__ModuleLoader__.load({
7
7
  let react = require("react");
8
8
  let _deepseek_ai_dsh_client_ui_primitives = require("@deepseek-ai/dsh-client-ui-primitives");
9
9
  let react_jsx_runtime = require("react/jsx-runtime");
10
+ let _deepseek_ai_dsh_client_connection_client = require("@deepseek-ai/dsh-client-connection/client");
10
11
  //#region src/config/fields.ts
11
12
  /** Field group ids -> locale key of the group heading. */
12
13
  const TOOL_PLUS_GROUP_LABELS = {
@@ -442,6 +443,64 @@ window.__ModuleLoader__.load({
442
443
  tool: "read",
443
444
  visibility: { requiresEnabled: "fetchEnabled" }
444
445
  },
446
+ {
447
+ name: "fetchReader",
448
+ kind: "select",
449
+ default: "auto",
450
+ labelKey: "fetchReader",
451
+ hintKey: "fetchReaderHint",
452
+ group: "fetch",
453
+ tool: "read",
454
+ options: [
455
+ {
456
+ value: "auto",
457
+ labelKey: "optReaderAuto"
458
+ },
459
+ {
460
+ value: "native",
461
+ labelKey: "optReaderNative"
462
+ },
463
+ {
464
+ value: "trafilatura",
465
+ labelKey: "optReaderTrafilatura"
466
+ },
467
+ {
468
+ value: "lynx",
469
+ labelKey: "optReaderLynx"
470
+ },
471
+ {
472
+ value: "parallel",
473
+ labelKey: "optReaderParallel"
474
+ },
475
+ {
476
+ value: "jina",
477
+ labelKey: "optReaderJina"
478
+ },
479
+ {
480
+ value: "browser",
481
+ labelKey: "optReaderBrowser"
482
+ }
483
+ ]
484
+ },
485
+ {
486
+ name: "browserReaderEnabled",
487
+ kind: "boolean",
488
+ default: true,
489
+ labelKey: "browserReaderEnabled",
490
+ hintKey: "browserReaderEnabledHint",
491
+ group: "fetch",
492
+ tool: "read"
493
+ },
494
+ {
495
+ name: "browserProbe",
496
+ kind: "action",
497
+ default: "",
498
+ labelKey: "browserProbe",
499
+ hintKey: "browserProbeHint",
500
+ group: "fetch",
501
+ tool: "read",
502
+ actionKey: "browser/detect"
503
+ },
445
504
  {
446
505
  name: "imagesBlockImages",
447
506
  kind: "boolean",
@@ -667,44 +726,84 @@ window.__ModuleLoader__.load({
667
726
  TOOL_PLUS_FIELDS.map((field) => field.name);
668
727
  //#endregion
669
728
  //#region src/client/rows.tsx
670
- /** Shared row control CSS, keyed by `data-plugin-css` and injected once. */
729
+ /**
730
+ * Shared settings-row controls for the tool-plus settings surfaces: number
731
+ * input, select pill, and toggle switch rows, styled against the app's
732
+ * `--dsw-alias-*` design tokens. Extracted from the Bash card so the Plugins
733
+ * card and the Tool Plus settings section render identical controls.
734
+ * @module @xiaoso/dsh-tool-plus/client
735
+ */
736
+ /**
737
+ * Shared row control CSS, keyed by `data-plugin-css` and injected once.
738
+ *
739
+ * Motion discipline: every moving control animates transform/opacity only
740
+ * (GPU-composited); color transitions fire on hover/focus state changes
741
+ * only. Everything is disabled under prefers-reduced-motion. All colors are
742
+ * `--dsw-alias-*` host tokens (or color-mix of them), so light/dark themes
743
+ * adapt automatically.
744
+ */
671
745
  const SETTINGS_ROWS_CSS = `
672
- .tp-row{display:flex;align-items:center;gap:12px;padding:12px 0;border-bottom:1px solid var(--dsw-alias-border-l2)}
746
+ .tp-row{display:flex;align-items:center;gap:12px;padding:12px 0;border-bottom:1px solid var(--dsw-alias-border-l1);transition:background .16s ease}
673
747
  .tp-row:last-child{border-bottom:none}
748
+ .tp-rowClickable{cursor:pointer}
674
749
  .tp-rowText{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px;padding-right:24px}
675
750
  .tp-rowTitle{font-size:14px;font-weight:400;line-height:22px;color:var(--dsw-alias-label-primary)}
676
751
  .tp-rowDesc{font-size:12px;font-weight:400;line-height:18px;color:var(--dsw-alias-label-tertiary)}
677
752
  .tp-badges{display:inline-flex;align-items:center;gap:8px}
678
- .tp-badge{border-radius:999px;padding:1px 8px;font-size:11px;line-height:17px;white-space:nowrap;font-weight:500;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary)}
679
- .tp-reset{border:none;background:none;padding:0;font:inherit;font-size:12px;line-height:1.5;color:var(--dsw-alias-label-secondary);cursor:pointer}
753
+ .tp-badge{display:inline-flex;align-items:center;gap:6px;padding:1px 0;font-size:11px;line-height:17px;white-space:nowrap;font-weight:500;color:var(--dsw-alias-label-tertiary)}
754
+ .tp-badge::before{content:'';flex:none;width:5px;height:5px;border-radius:999px;background:var(--dsw-alias-state-business-primary)}
755
+ .tp-reset{position:relative;border:none;background:none;padding:2px 0;font:inherit;font-size:12px;line-height:1.5;color:var(--dsw-alias-label-secondary);cursor:pointer;transition:color .16s ease}
756
+ .tp-reset::after{content:'';position:absolute;left:0;right:0;bottom:0;height:1px;border-radius:1px;background:currentColor;opacity:0;transform:scaleX(.6);transform-origin:left center;transition:opacity .16s ease,transform .2s cubic-bezier(.22,1,.36,1)}
680
757
  .tp-reset:hover:not(:disabled){color:var(--dsw-alias-label-primary)}
758
+ .tp-reset:hover:not(:disabled)::after{opacity:.6;transform:scaleX(1)}
681
759
  .tp-reset:disabled{cursor:default}
682
- .tp-numberField{display:flex;flex-direction:column;gap:6px;padding:12px 0;border-bottom:1px solid var(--dsw-alias-border-l2)}
683
- .tp-numberField:last-child{border-bottom:none}
684
- .tp-numberHead{display:flex;align-items:center;gap:8px}
685
- .tp-numberLabel{flex:1;min-width:0;font-size:13px;font-weight:500;line-height:1.5;color:var(--dsw-alias-label-primary)}
686
- .tp-input{height:34px;padding:0 12px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-3);font:inherit;font-size:13px;line-height:1.5;color:var(--dsw-alias-label-primary);width:100%;box-sizing:border-box}
687
- .tp-input:focus-visible{outline:none;border-color:var(--dsw-alias-brand-primary)}
760
+ .tp-reset:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:2px;border-radius:2px}
761
+ .tp-input{flex:none;width:128px;height:34px;padding:0 11px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-1);font:inherit;font-size:13px;line-height:1.5;color:var(--dsw-alias-label-primary);box-sizing:border-box;transition:border-color .16s ease}
762
+ .tp-input:hover:not(:disabled):not(:focus-visible){border-color:var(--dsw-alias-border-l3)}
763
+ .tp-input:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:1px;border-color:var(--dsw-alias-border-l3)}
688
764
  .tp-input:disabled{color:var(--dsw-alias-label-tertiary);cursor:default}
689
765
  .tp-input.tp-invalid{border-color:var(--dsw-alias-label-error)}
690
- .tp-invalidMsg{margin:0;font-size:12px;line-height:1.5;color:var(--dsw-alias-label-error)}
691
- .tp-hint{margin:0;font-size:12px;line-height:1.5;color:var(--dsw-alias-label-tertiary)}
692
- .tp-selectTrigger{display:inline-flex;align-items:center;gap:12px;height:36px;padding:0 14px;border:none;border-radius:18px;background:var(--dsw-alias-bg-module-platform);font:inherit;font-size:14px;line-height:22px;color:var(--dsw-alias-label-primary);cursor:pointer;flex-shrink:0}
766
+ .tp-input.tp-invalid:focus-visible{outline-color:var(--dsw-alias-state-error-primary)}
767
+ .tp-invalidText{color:var(--dsw-alias-label-error);animation:tp-soft-in .18s ease both}
693
768
  .tp-selectAnchor{display:inline-flex;flex-shrink:0}
694
- .tp-selectTrigger:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover)}
695
- .tp-selectTrigger:disabled{cursor:default}
696
- .tp-selectTrigger:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}
697
- .tp-selectChevron{flex:none;color:var(--dsw-alias-label-tertiary);display:inline-flex;transition:transform .16s}
769
+ .tp-selectTrigger{display:inline-flex;align-items:center;gap:8px;height:34px;padding:0 14px;border:1px solid var(--dsw-alias-border-l2);border-radius:999px;background:var(--dsw-alias-bg-module-platform);font:inherit;font-size:13px;line-height:20px;color:var(--dsw-alias-label-primary);cursor:pointer;flex-shrink:0;transition:background .16s ease,border-color .16s ease,transform .1s ease}
770
+ .tp-selectTrigger:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover);border-color:var(--dsw-alias-border-l3)}
771
+ .tp-selectTrigger:active:not(:disabled){transform:scale(.97)}
772
+ .tp-selectTrigger:disabled{opacity:.55;cursor:default}
773
+ .tp-selectTrigger:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:1px}
774
+ .tp-selectChevron{flex:none;color:var(--dsw-alias-label-tertiary);display:inline-flex;transition:transform .2s cubic-bezier(.22,1,.36,1)}
698
775
  .tp-selectTrigger[aria-expanded="true"] .tp-selectChevron{transform:rotate(180deg)}
699
- .tp-switch{position:relative;width:36px;height:20px;flex:none}
700
- .tp-switch input{position:absolute;opacity:0;width:100%;height:100%;margin:0;cursor:pointer}
701
- .tp-switchTrack{position:absolute;inset:0;border-radius:999px;background:var(--dsw-alias-border-l2);transition:background .16s;pointer-events:none}
776
+ .tp-switch{position:relative;width:40px;height:24px;flex:none}
777
+ .tp-switch input{position:absolute;opacity:0;width:100%;height:100%;margin:0;cursor:pointer;z-index:1}
778
+ .tp-switchTrack{position:absolute;inset:0;border-radius:999px;background:var(--dsw-alias-border-l2);transition:background .18s ease;pointer-events:none}
779
+ .tp-row:hover .tp-switchTrack{background:var(--dsw-alias-border-l3)}
702
780
  .tp-switch input:checked+.tp-switchTrack{background:var(--dsw-alias-brand-primary)}
703
- .tp-switch input:focus-visible+.tp-switchTrack{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}
704
- .tp-thumb{position:absolute;top:2px;left:2px;width:16px;height:16px;border-radius:999px;background:var(--dsw-alias-bg-layer-3);transition:transform .16s;pointer-events:none}
705
- .tp-switch input:checked~.tp-thumb{transform:translateX(16px)}
781
+ .tp-row:hover .tp-switch input:checked+.tp-switchTrack{background:var(--dsw-alias-brand-primary)}
782
+ .tp-switch input:focus-visible+.tp-switchTrack{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:2px}
783
+ .tp-thumb{position:absolute;top:3px;left:3px;width:18px;height:18px;border-radius:999px;background:var(--dsw-alias-bg-layer-3);box-shadow:0 1px 3px rgba(0,0,0,.14),0 1px 1px rgba(0,0,0,.08);transition:transform .2s cubic-bezier(.34,1.3,.5,1);transform-origin:left center;pointer-events:none}
784
+ .tp-switch input:checked~.tp-thumb{transform:translateX(16px);transform-origin:right center}
785
+ .tp-switch input:active:not(:disabled)~.tp-thumb{transform:scaleX(1.2)}
786
+ .tp-switch input:checked:active:not(:disabled)~.tp-thumb{transform:translateX(16px) scaleX(1.2)}
787
+ .tp-switch input:disabled{cursor:default}
788
+ .tp-switch input:disabled~.tp-switchTrack,.tp-switch input:disabled~.tp-thumb{opacity:.45}
789
+ .tp-actionField{display:flex;flex-direction:column;gap:8px;padding:12px 0;border-bottom:1px solid var(--dsw-alias-border-l1)}
790
+ .tp-actionField:last-child{border-bottom:none}
791
+ .tp-actionHead{display:flex;align-items:center;gap:12px}
792
+ .tp-actionInfo{flex:1;min-width:0;display:flex;flex-direction:column;gap:2px}
793
+ .tp-actionTitle{font-size:14px;font-weight:400;line-height:22px;color:var(--dsw-alias-label-primary)}
794
+ .tp-actionDesc{font-size:12px;font-weight:400;line-height:18px;color:var(--dsw-alias-label-tertiary)}
795
+ .tp-actionButton{appearance:none;flex:none;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-1);padding:5px 14px;font:inherit;font-size:13px;line-height:1.5;color:var(--dsw-alias-label-primary);cursor:pointer;transition:background .16s ease,border-color .16s ease,transform .1s ease}
796
+ .tp-actionButton:hover:not(:disabled){background:var(--dsw-alias-interactive-bg-hover);border-color:var(--dsw-alias-border-l3)}
797
+ .tp-actionButton:active:not(:disabled){transform:scale(.97)}
798
+ .tp-actionButton:disabled{opacity:.55;cursor:default}
799
+ .tp-actionButton:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:1px}
800
+ .tp-actionResult{margin:0;font-size:12px;line-height:1.5;white-space:pre-wrap;overflow-wrap:anywhere;animation:tp-soft-in .22s cubic-bezier(.22,1,.36,1) both}
801
+ .tp-actionResult[data-state="ok"]{color:var(--dsw-alias-label-secondary)}
802
+ .tp-actionResult[data-state="error"]{color:var(--dsw-alias-label-error)}
803
+ @keyframes tp-soft-in{from{opacity:0;transform:translateY(4px)}to{opacity:1;transform:none}}
706
804
  @media (prefers-reduced-motion: reduce){
707
- .tp-switch,.tp-switchTrack,.tp-thumb,.tp-selectTrigger,.tp-selectChevron{transition:none}
805
+ .tp-row,.tp-reset,.tp-reset::after,.tp-input,.tp-selectTrigger,.tp-selectChevron,.tp-switchTrack,.tp-thumb,.tp-actionButton{transition:none}
806
+ .tp-invalidText,.tp-actionResult{animation:none}
708
807
  }
709
808
  `;
710
809
  /** Inject the shared row stylesheet once per page. */
@@ -723,46 +822,53 @@ window.__ModuleLoader__.load({
723
822
  function hasOwn(obj, key) {
724
823
  return typeof obj === "object" && obj !== null && Object.prototype.hasOwnProperty.call(obj, key);
725
824
  }
726
- /** Number field: Header (label + override badge + reset), Input, Hint below. */
825
+ /**
826
+ * Number row: same left-text / right-control anatomy as toggle and select
827
+ * rows, with the input right-aligned at a fixed width. The label is wired to
828
+ * the input (htmlFor) and clicking anywhere on the row focuses the input.
829
+ */
727
830
  function NumberRow(props) {
728
831
  const { t, control, disabled, onEdit, onReset } = props;
832
+ const inputId = (0, react.useId)();
729
833
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
730
- className: "tp-numberField",
834
+ className: "tp-row" + (disabled ? "" : " tp-rowClickable"),
835
+ onClick: (event) => {
836
+ if (event.target.closest("input,button,select,textarea,a")) return;
837
+ event.currentTarget.querySelector(".tp-input")?.focus();
838
+ },
731
839
  children: [
732
840
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
733
- className: "tp-numberHead",
841
+ className: "tp-rowText",
734
842
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("label", {
735
- className: "tp-numberLabel",
843
+ className: "tp-rowTitle",
844
+ htmlFor: inputId,
736
845
  children: t(control.labelKey)
737
- }), control.overridden ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
738
- className: "tp-badges",
739
- children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
740
- className: "tp-badge",
741
- children: t("overridden")
742
- }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
743
- type: "button",
744
- className: "tp-reset",
745
- disabled,
746
- onClick: () => onReset(control.field),
747
- children: t("reset")
748
- })]
749
- }) : null]
846
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
847
+ className: "tp-rowDesc" + (control.invalid ? " tp-invalidText" : ""),
848
+ role: control.invalid ? "status" : void 0,
849
+ children: control.invalid ? t("invalidNumber") : t(control.hintKey)
850
+ })]
750
851
  }),
852
+ control.overridden ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
853
+ className: "tp-badges",
854
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
855
+ className: "tp-badge",
856
+ children: t("overridden")
857
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
858
+ type: "button",
859
+ className: "tp-reset",
860
+ disabled,
861
+ onClick: () => onReset(control.field),
862
+ children: t("reset")
863
+ })]
864
+ }) : null,
751
865
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("input", {
866
+ id: inputId,
752
867
  type: "number",
753
- "aria-label": t(control.labelKey),
754
868
  className: "tp-input" + (control.invalid ? " tp-invalid" : ""),
755
869
  disabled,
756
870
  value: control.text,
757
871
  onChange: (e) => onEdit(control.field, e.target.value)
758
- }),
759
- control.invalid ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
760
- className: "tp-invalidMsg",
761
- role: "status",
762
- children: t("invalidNumber")
763
- }) : /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
764
- className: "tp-hint",
765
- children: t(control.hintKey)
766
872
  })
767
873
  ]
768
874
  });
@@ -777,7 +883,11 @@ window.__ModuleLoader__.load({
777
883
  }));
778
884
  const close = () => onOpenChange(null);
779
885
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
780
- className: "tp-row",
886
+ className: "tp-row" + (disabled ? "" : " tp-rowClickable"),
887
+ onClick: (event) => {
888
+ if (event.target.closest("input,button,select,textarea,a")) return;
889
+ event.currentTarget.querySelector(".tp-selectTrigger")?.click();
890
+ },
781
891
  children: [
782
892
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
783
893
  className: "tp-rowText",
@@ -831,7 +941,11 @@ window.__ModuleLoader__.load({
831
941
  function ToggleRow(props) {
832
942
  const { t, control, disabled, onToggle, onReset } = props;
833
943
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
834
- className: "tp-row",
944
+ className: "tp-row" + (disabled ? "" : " tp-rowClickable"),
945
+ onClick: (event) => {
946
+ if (event.target.closest("input,button,select,textarea,a")) return;
947
+ event.currentTarget.querySelector(".tp-switch input")?.click();
948
+ },
835
949
  children: [
836
950
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
837
951
  className: "tp-rowText",
@@ -873,6 +987,42 @@ window.__ModuleLoader__.load({
873
987
  ]
874
988
  });
875
989
  }
990
+ /**
991
+ * Action row: Title + desc stacked on left, a button on the right that calls
992
+ * the host RPC endpoint named by `control.actionKey` (via the Connection
993
+ * channel; the caller owns the RPC). The result line renders below, colored
994
+ * by outcome (`ok` / `error`).
995
+ */
996
+ function ActionRow(props) {
997
+ const { t, control, disabled, running, result, onRun } = props;
998
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
999
+ className: "tp-actionField",
1000
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1001
+ className: "tp-actionHead",
1002
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1003
+ className: "tp-actionInfo",
1004
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1005
+ className: "tp-actionTitle",
1006
+ children: t(control.labelKey)
1007
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
1008
+ className: "tp-actionDesc",
1009
+ children: t(control.hintKey)
1010
+ })]
1011
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1012
+ type: "button",
1013
+ className: "tp-actionButton",
1014
+ disabled: disabled || running,
1015
+ onClick: () => onRun(control.actionKey),
1016
+ children: running ? t("browserProbeRunning") : t(control.labelKey)
1017
+ })]
1018
+ }), result !== null ? /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
1019
+ className: "tp-actionResult",
1020
+ role: "status",
1021
+ "data-state": result.ok ? "ok" : "error",
1022
+ children: result.text
1023
+ }) : null]
1024
+ });
1025
+ }
876
1026
  //#endregion
877
1027
  //#region src/client/BashPlusCard.tsx
878
1028
  /**
@@ -952,11 +1102,13 @@ window.__ModuleLoader__.load({
952
1102
  .tp-card.tp-open{background:var(--dsw-alias-bg-layer-2);border-color:var(--dsw-alias-label-dimmed)}
953
1103
  .tp-header{width:100%;appearance:none;border:0;background:none;font:inherit;color:inherit;text-align:left;cursor:pointer;display:flex;align-items:center;gap:12px;padding:14px 16px;border-radius:12px}
954
1104
  .tp-header:active{background:var(--dsw-alias-bg-layer-2)}
955
- .tp-header:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:-2px}
1105
+ .tp-header:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:-2px}
956
1106
  .tp-headText{flex:1;min-width:0;display:flex;flex-direction:column;gap:4px}
957
1107
  .tp-name{font-size:15px;font-weight:600;line-height:1.4;color:var(--dsw-alias-label-primary)}
958
1108
  .tp-desc{font-size:13px;line-height:1.5;color:var(--dsw-alias-label-tertiary)}
959
- .tp-pending{flex:none;border-radius:999px;padding:1px 8px;font-size:11px;line-height:17px;font-weight:500;white-space:nowrap;background:var(--dsw-alias-bg-module-platform);color:var(--dsw-alias-label-secondary)}
1109
+ .tp-pending{display:inline-flex;align-items:center;gap:6px;flex:none;border-radius:999px;padding:2px 10px;font-size:11px;line-height:17px;font-weight:500;white-space:nowrap;background:var(--dsw-alias-state-business-tertiary);color:var(--dsw-alias-state-business-primary)}
1110
+ .tp-pending::before{content:'';flex:none;width:6px;height:6px;border-radius:999px;background:var(--dsw-alias-state-business-primary);animation:tp-pulse 1.8s ease-in-out infinite}
1111
+ @keyframes tp-pulse{0%,100%{opacity:.35;transform:scale(.85)}50%{opacity:1;transform:scale(1)}}
960
1112
  .tp-chevron{flex:none;color:var(--dsw-alias-label-tertiary);transition:transform .16s;display:inline-flex}
961
1113
  .tp-chevron.tp-open{transform:rotate(180deg)}
962
1114
  .tp-body{border-top:1px solid var(--dsw-alias-border-l2);margin:0 16px;padding-top:4px;display:grid;grid-template-rows:0fr;opacity:0;transition:grid-template-rows .22s cubic-bezier(.22,.61,.36,1),opacity .16s ease}
@@ -974,10 +1126,12 @@ window.__ModuleLoader__.load({
974
1126
  .tp-discard{border-color:var(--dsw-alias-border-l2);background:none;color:var(--dsw-alias-label-secondary)}
975
1127
  .tp-discard:hover:not(:disabled){color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-dimmed)}
976
1128
  .tp-save{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3)}
1129
+ .tp-save:hover:not(:disabled){background:var(--dsw-alias-button-primary-hover)}
977
1130
  .tp-discard:disabled,.tp-save:disabled{opacity:.4;cursor:default}
978
- .tp-discard:focus-visible,.tp-save:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}
1131
+ .tp-discard:focus-visible,.tp-save:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:1px}
979
1132
  @media (prefers-reduced-motion: reduce){
980
1133
  .tp-card,.tp-header,.tp-chevron,.tp-body,.tp-thumb,.tp-switchTrack,.tp-discard,.tp-save{transition:none}
1134
+ .tp-pending::before{animation:none}
981
1135
  }
982
1136
  `;
983
1137
  /** Inject the card stylesheet once per page; the loader removes plugin-owned style tags on unload. */
@@ -1473,6 +1627,113 @@ window.__ModuleLoader__.load({
1473
1627
  });
1474
1628
  }
1475
1629
  //#endregion
1630
+ //#region src/tools/shared/browser-rpc-channel.ts
1631
+ /**
1632
+ * Shared RPC channel constants for the browser-detection round trip between
1633
+ * the settings panel (client half) and the host. Both halves import this
1634
+ * module so the channel/endpoint never drift.
1635
+ *
1636
+ * The channel rides the official `@deepseek-ai/dsh-client-connection` generic
1637
+ * RPC transport (host registers with `ctx.connection.rpc.handle`; the client
1638
+ * calls via `createWebConnectionRpc().call`). Channel names must match
1639
+ * `/^\/[A-Za-z0-9._~-]+$/` and `/api` is reserved — `/tool-plus` is ours.
1640
+ * @module @xiaoso/dsh-tool-plus/browser-rpc-channel
1641
+ */
1642
+ /** Logical RPC channel registered by the host half of this plugin. */
1643
+ const TOOL_PLUS_RPC_CHANNEL = "/tool-plus";
1644
+ //#endregion
1645
+ //#region src/client/web-connection-rpc.ts
1646
+ /**
1647
+ * Browser caller for the plugin's Connection RPC channel.
1648
+ *
1649
+ * The official `@deepseek-ai/dsh-client-connection/client` package ships the
1650
+ * protocol primitives as types (`RpcFetch`, `ClientConnectionRpc`, `RpcResult`)
1651
+ * and the `RpcId` brand, but its published `createWebConnectionRpc` factory is
1652
+ * internal to the bundle — not re-exported. This module re-implements that
1653
+ * factory against the same public wire contract (docs/api-gateway.md, the
1654
+ * client `rpc.ts` source): a `POST {origin}/{channel}/{endpoint}` with a
1655
+ * `client-request` JSON envelope, answered by a `server-response` envelope
1656
+ * echoing the rpcId. Host-side registration uses the official
1657
+ * `ctx.connection.rpc.handle` — this file only mirrors the caller half.
1658
+ * @module @xiaoso/dsh-tool-plus/client/web-connection-rpc
1659
+ */
1660
+ const INTERNAL_BASE = "http://dsh.internal";
1661
+ const CHANNEL_PATTERN = /^\/[A-Za-z0-9._~-]+$/;
1662
+ const ENDPOINT_SEGMENT_PATTERN = /^[A-Za-z0-9_$.-]+$/;
1663
+ function randomUuid() {
1664
+ const g = globalThis;
1665
+ if (g.crypto?.randomUUID !== void 0) return g.crypto.randomUUID();
1666
+ return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
1667
+ const r = Math.random() * 16 | 0;
1668
+ return (c === "x" ? r : r & 3 | 8).toString(16);
1669
+ });
1670
+ }
1671
+ function isRecord(value) {
1672
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1673
+ }
1674
+ function parseConnectionResponse(value) {
1675
+ if (!isRecord(value) || value.type !== "server-response" || typeof value.rpcId !== "string") throw new TypeError("connection: invalid server-response envelope");
1676
+ const result = value.result;
1677
+ if (!isRecord(result)) throw new TypeError("connection: invalid server-response result");
1678
+ if (result.ok === true) return {
1679
+ rpcId: value.rpcId,
1680
+ result: {
1681
+ ok: true,
1682
+ value: result.value
1683
+ }
1684
+ };
1685
+ if (result.ok !== false || !isRecord(result.error)) throw new TypeError("connection: invalid server-response result");
1686
+ const error = result.error;
1687
+ if (typeof error.code !== "string" || typeof error.message !== "string" || !isRecord(error.details)) throw new TypeError("connection: invalid server-response failure");
1688
+ return {
1689
+ rpcId: value.rpcId,
1690
+ result: {
1691
+ ok: false,
1692
+ error: {
1693
+ code: error.code,
1694
+ message: error.message,
1695
+ details: error.details
1696
+ }
1697
+ }
1698
+ };
1699
+ }
1700
+ function resolveBase() {
1701
+ const location = globalThis.location;
1702
+ return location?.origin !== void 0 && location.origin !== "null" ? location.origin : INTERNAL_BASE;
1703
+ }
1704
+ function assertTarget(channel, endpoint) {
1705
+ const segments = endpoint.split("/");
1706
+ if (!CHANNEL_PATTERN.test(channel) || segments.some((segment) => segment === "" || segment === "." || segment === ".." || !ENDPOINT_SEGMENT_PATTERN.test(segment))) throw new Error(`connection: invalid RPC target ${JSON.stringify(`${channel}/${endpoint}`)}`);
1707
+ }
1708
+ /**
1709
+ * Create the browser-backed generic RPC caller for the plugin's channel
1710
+ * (the `/tool-plus` host registration). Wire-compatible with the official
1711
+ * ClientConnectionRpc contract; only the transport override differs.
1712
+ * @param doFetch - transport override; defaults to the page's global fetch.
1713
+ */
1714
+ function createWebConnectionRpc(doFetch) {
1715
+ const send = doFetch ?? ((input, init) => globalThis.fetch(input, init));
1716
+ return { async call(channel, endpoint, payload, signal) {
1717
+ assertTarget(channel, endpoint);
1718
+ const rpcId = (0, _deepseek_ai_dsh_client_connection_client.RpcId)(randomUuid());
1719
+ const response = await send(new URL(`${channel}/${endpoint}`, resolveBase()), {
1720
+ method: "POST",
1721
+ headers: { "content-type": "application/json" },
1722
+ body: JSON.stringify({
1723
+ type: "client-request",
1724
+ rpcId,
1725
+ method: endpoint,
1726
+ payload
1727
+ }),
1728
+ ...signal === void 0 ? {} : { signal }
1729
+ });
1730
+ if (!response.ok) throw new Error(`transport failure for ${channel}/${endpoint}: HTTP ${response.status}`);
1731
+ const full = parseConnectionResponse(await response.json());
1732
+ if (full.rpcId !== rpcId) throw new Error(`rpcId mismatch for ${endpoint}: sent ${rpcId}, got ${full.rpcId}`);
1733
+ return full.result;
1734
+ } };
1735
+ }
1736
+ //#endregion
1476
1737
  //#region src/client/forms.ts
1477
1738
  /**
1478
1739
  * Generic staged-form model over the plugin's flat settings namespace,
@@ -1643,7 +1904,7 @@ window.__ModuleLoader__.load({
1643
1904
  }
1644
1905
  if (field.kind === "number") group.numbers.push(renderNumber(field, numbers, value, snap, defaults));
1645
1906
  else if (field.kind === "select") group.selects.push(renderSelect(field, selects, value, snap, defaults));
1646
- else group.toggles.push(renderToggle(field, toggles, value, snap, defaults));
1907
+ else if (field.kind === "boolean") group.toggles.push(renderToggle(field, toggles, value, snap, defaults));
1647
1908
  }
1648
1909
  return order.map((key) => map.get(key));
1649
1910
  }, [
@@ -1780,40 +2041,76 @@ window.__ModuleLoader__.load({
1780
2041
  * without keeping hidden panels mounted — the same flat schema the Bash card
1781
2042
  * edits, from the same single-source field table.
1782
2043
  *
2044
+ * `action`-kind fields (e.g. 探测浏览器) are not part of the staged form:
2045
+ * they run a host RPC endpoint over the official Connection channel
2046
+ * (`/tool-plus`), with per-field running/result state kept at the page level.
2047
+ *
1783
2048
  * Tab chrome mirrors the official Plugins settings section
1784
2049
  * (ui-settings-plugins): `role="tablist"` + tab/panel roles, aria wiring,
1785
2050
  * and arrow/Home/End keyboard roaming.
2051
+ *
2052
+ * The page is pinned to the host settings pane: the heading, intro and tablist
2053
+ * stay put, the save bar sits at the bottom, and the tab panel is the only
2054
+ * scroller — so the scrollbar spans just the band between them.
1786
2055
  * @module @xiaoso/dsh-tool-plus/client
1787
2056
  */
1788
- /** Page CSS, keyed by `data-plugin-css` and injected once. */
2057
+ /**
2058
+ * Page CSS, keyed by `data-plugin-css` and injected once.
2059
+ *
2060
+ * Design language: official settings-section rhythm (18/600 heading, 13px
2061
+ * tertiary intro, 760px content column) on the host's `--dsw-alias-*`
2062
+ * tokens, so both themes adapt automatically. Tab chrome is the official
2063
+ * underline tablist (like the Plugins section), upgraded with a single
2064
+ * sliding brand-blue underline; group cards float above the modal surface
2065
+ * (layer-3). No backdrop-filter anywhere — solid token colors only.
2066
+ * All motion is transform/opacity only (GPU-composited): the underline
2067
+ * FLIP is driven from JS, entrance stagger and panel fades are CSS
2068
+ * keyframes, and the unsaved dot pulses opacity/scale. Everything shuts
2069
+ * off under prefers-reduced-motion (CSS here, matchMedia for the
2070
+ * JS-driven underline).
2071
+ */
1789
2072
  const CSS = `
1790
- .tps{display:flex;flex-direction:column;gap:16px;padding:20px 24px}
1791
- .tps-heading{margin:0;font-size:18px;font-weight:600;line-height:26px;color:var(--dsw-alias-label-primary)}
1792
- .tps-intro{margin:0;font-size:13px;line-height:20px;color:var(--dsw-alias-label-tertiary)}
1793
- .tps-tabs{display:flex;flex-wrap:wrap;gap:4px;padding:4px;border:1px solid var(--dsw-alias-border-l2);border-radius:12px;background:var(--dsw-alias-bg-layer-3)}
1794
- .tps-tab{appearance:none;border:none;background:none;font:inherit;font-size:13px;line-height:20px;color:var(--dsw-alias-label-secondary);padding:6px 14px;border-radius:8px;cursor:pointer;transition:background .14s,color .14s}
1795
- .tps-tab:hover:not([data-active="true"]){background:var(--dsw-alias-interactive-bg-hover);color:var(--dsw-alias-label-primary)}
1796
- .tps-tab[data-active="true"]{background:var(--dsw-alias-bg-layer-1);color:var(--dsw-alias-label-primary);box-shadow:0 1px 2px var(--dsw-alias-border-l2)}
1797
- .tps-tab:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}
1798
- .tps-panel{display:flex;flex-direction:column;gap:12px}
1799
- .tps-group{display:flex;flex-direction:column;gap:0;padding:4px 0}
1800
- .tps-groupTitle{font-size:13px;font-weight:600;line-height:20px;color:var(--dsw-alias-label-primary);margin:0 0 4px}
1801
- .tps-empty{border:1px dashed var(--dsw-alias-border-l2);border-radius:12px;padding:20px 16px;display:flex;flex-direction:column;gap:4px;background:var(--dsw-alias-bg-layer-3)}
2073
+ .tps-section{box-sizing:border-box;display:flex;flex-direction:column;gap:16px;padding-top:2px}
2074
+ .tps-heading{margin:0;font-size:18px;font-weight:600;line-height:26px;color:var(--dsw-alias-label-primary);animation:tps-rise .38s cubic-bezier(.22,1,.36,1) both}
2075
+ .tps-intro{margin:0;font-size:13px;line-height:20px;color:var(--dsw-alias-label-tertiary);max-width:64ch;animation:tps-rise .38s cubic-bezier(.22,1,.36,1) .05s both}
2076
+ .tps-tabs{position:relative;display:flex;align-items:flex-end;gap:22px;border-bottom:1px solid var(--dsw-alias-border-l2);margin-top:2px;animation:tps-rise .38s cubic-bezier(.22,1,.36,1) .1s both}
2077
+ .tps-tab{position:relative;appearance:none;border:none;background:none;font:inherit;font-size:13px;line-height:20px;color:var(--dsw-alias-label-tertiary);padding:7px 1px 9px;cursor:pointer;white-space:nowrap;transition:color .16s ease}
2078
+ .tps-tab:hover:not([data-active="true"]){color:var(--dsw-alias-label-primary)}
2079
+ .tps-tab[data-active="true"]{color:var(--dsw-alias-label-primary)}
2080
+ .tps-tab:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:2px;border-radius:2px}
2081
+ .tps-tabIndicator{position:absolute;bottom:-1px;left:0;height:2px;border-radius:2px 2px 0 0;background:var(--dsw-alias-state-business-primary);pointer-events:none;transform-origin:left center;will-change:transform}
2082
+ .tps-panel{display:flex;flex:1 1 auto;flex-direction:column;gap:14px;min-width:0;min-height:0;overflow-y:auto;overscroll-behavior:contain;scrollbar-gutter:stable;scrollbar-width:thin;animation:tps-panel-in .24s cubic-bezier(.22,1,.36,1) both}
2083
+ .tps-panel::-webkit-scrollbar{width:10px}
2084
+ .tps-panel::-webkit-scrollbar-track{background:transparent}
2085
+ .tps-panel::-webkit-scrollbar-thumb{border:3px solid transparent;border-radius:999px;background-color:color-mix(in srgb, var(--dsw-alias-label-tertiary) 45%, transparent);background-clip:content-box}
2086
+ .tps-panel::-webkit-scrollbar-thumb:hover{background-color:color-mix(in srgb, var(--dsw-alias-label-tertiary) 75%, transparent)}
2087
+ .tps-group{display:flex;flex-direction:column}
2088
+ .tps-groupTitle{font-size:13px;font-weight:600;line-height:20px;color:var(--dsw-alias-label-primary);margin:0 0 6px;padding:0 2px}
2089
+ .tps-card{display:flex;flex-direction:column;border:1px solid var(--dsw-alias-border-l2);border-radius:12px;background:var(--dsw-alias-bg-layer-3);padding:0 16px}
2090
+ .tps-empty{border:1px dashed var(--dsw-alias-border-l3);border-radius:12px;padding:24px 16px;display:flex;flex-direction:column;align-items:center;text-align:center;gap:4px}
1802
2091
  .tps-emptyTitle{margin:0;font-size:14px;font-weight:600;line-height:22px;color:var(--dsw-alias-label-primary)}
1803
2092
  .tps-emptyHint{margin:0;font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary)}
1804
- .tps-readOnly{margin:0;font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary)}
1805
- .tps-footer{display:flex;align-items:center;justify-content:flex-end;gap:8px;padding:12px 0 0;border-top:1px solid var(--dsw-alias-border-l2)}
2093
+ .tps-readOnly{margin:0;padding:8px 12px;border:1px solid var(--dsw-alias-border-l2);border-radius:8px;background:var(--dsw-alias-bg-layer-3);font-size:12px;line-height:18px;color:var(--dsw-alias-label-tertiary)}
2094
+ .tps-footer{flex:none;display:flex;align-items:center;justify-content:flex-end;gap:10px;padding:12px 0;border-top:1px solid var(--dsw-alias-border-l2)}
1806
2095
  .tps-failed{flex:1;min-width:0;margin:0;font-size:12px;line-height:1.5;color:var(--dsw-alias-label-error)}
1807
2096
  .tps-applies{flex:1;min-width:0;font-size:12px;line-height:1.5;color:var(--dsw-alias-label-tertiary)}
1808
- .tps-discard,.tps-save{appearance:none;border:1px solid transparent;border-radius:8px;padding:5px 14px;font:inherit;font-size:13px;line-height:1.5;cursor:pointer;transition:transform .1s ease}
2097
+ .tps-unsaved{display:inline-flex;align-items:center;gap:6px;flex:none;border-radius:999px;padding:2px 10px;font-size:11px;line-height:17px;font-weight:500;color:var(--dsw-alias-state-business-primary);background:var(--dsw-alias-state-business-tertiary)}
2098
+ .tps-unsavedDot{flex:none;width:6px;height:6px;border-radius:999px;background:var(--dsw-alias-state-business-primary);animation:tps-pulse 1.8s ease-in-out infinite}
2099
+ .tps-discard,.tps-save{appearance:none;border:1px solid transparent;border-radius:8px;padding:5px 14px;font:inherit;font-size:13px;line-height:1.5;cursor:pointer;transition:transform .1s ease,background .16s ease,border-color .16s ease,color .16s ease}
1809
2100
  .tps-discard:active:not(:disabled),.tps-save:active:not(:disabled){transform:scale(.97)}
1810
2101
  .tps-discard{border-color:var(--dsw-alias-border-l2);background:none;color:var(--dsw-alias-label-secondary)}
1811
- .tps-discard:hover:not(:disabled){color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-label-dimmed)}
2102
+ .tps-discard:hover:not(:disabled){color:var(--dsw-alias-label-primary);border-color:var(--dsw-alias-border-l4)}
1812
2103
  .tps-save{background:var(--dsw-alias-label-primary);color:var(--dsw-alias-bg-layer-3)}
2104
+ .tps-save:hover:not(:disabled){background:var(--dsw-alias-button-primary-hover)}
1813
2105
  .tps-discard:disabled,.tps-save:disabled{opacity:.4;cursor:default}
1814
- .tps-discard:focus-visible,.tps-save:focus-visible{outline:2px solid var(--dsw-alias-brand-primary);outline-offset:1px}
2106
+ .tps-discard:focus-visible,.tps-save:focus-visible{outline:2px solid var(--dsw-alias-state-business-primary);outline-offset:1px}
2107
+ @keyframes tps-rise{from{opacity:0;transform:translateY(8px)}to{opacity:1;transform:none}}
2108
+ @keyframes tps-panel-in{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}
2109
+ @keyframes tps-pulse{0%,100%{opacity:.35;transform:scale(.85)}50%{opacity:1;transform:scale(1)}}
1815
2110
  @media (prefers-reduced-motion: reduce){
2111
+ .tps-heading,.tps-intro,.tps-tabs,.tps-panel,.tps-unsavedDot{animation:none}
1816
2112
  .tps-tab,.tps-discard,.tps-save{transition:none}
2113
+ .tps-tabIndicator{will-change:auto}
1817
2114
  }
1818
2115
  `;
1819
2116
  /** Inject the section stylesheet once per page. */
@@ -1833,6 +2130,17 @@ window.__ModuleLoader__.load({
1833
2130
  const TOOL_TABS = TOOL_PLUS_TABS;
1834
2131
  /** All fields the page edits (one shared form). */
1835
2132
  const ALL_FIELDS = TOOL_PLUS_FIELDS;
2133
+ /** Floor for the pinned page height, so an unusually short pane never collapses the panel. */
2134
+ const MIN_PAGE_HEIGHT = 240;
2135
+ /**
2136
+ * Nearest ancestor that scrolls vertically — the host settings pane hosting
2137
+ * this section. The pane is app chrome with no stable class, so it is found by
2138
+ * walking up from the section root.
2139
+ */
2140
+ function scrollPaneOf(element) {
2141
+ for (let node = element.parentElement; node !== null; node = node.parentElement) if (/(auto|scroll|overlay)/.test(getComputedStyle(node).overflowY)) return node;
2142
+ return null;
2143
+ }
1836
2144
  /** Current effective value of a boolean/select control (staged or resolved). */
1837
2145
  function controlValue(form, field) {
1838
2146
  for (const group of form.groups) {
@@ -1853,10 +2161,11 @@ window.__ModuleLoader__.load({
1853
2161
  * Render one tool tab's panel content: fields in table order (master
1854
2162
  * switches first, per their declaration order), grouped under their group
1855
2163
  * heading, or the no-config placeholder for tools without global settings.
1856
- * Fields whose visibility condition is unmet are omitted.
2164
+ * Fields whose visibility condition is unmet are omitted. `action` fields
2165
+ * render their own button row (host RPC via the caller's `run`).
1857
2166
  */
1858
2167
  function ToolTabPanel(props) {
1859
- const { tab, t, form, writable, openSelect, onOpenChange } = props;
2168
+ const { tab, t, form, writable, openSelect, onOpenChange, actionState, actionResults, runAction } = props;
1860
2169
  if (tab.fields.length === 0) return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
1861
2170
  className: "tps-empty",
1862
2171
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", {
@@ -1904,32 +2213,51 @@ window.__ModuleLoader__.load({
1904
2213
  children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("h4", {
1905
2214
  className: "tps-groupTitle",
1906
2215
  children: t(TOOL_PLUS_GROUP_LABELS[group.group] ?? "title")
1907
- }), group.fields.map((field) => {
1908
- const entry = controlOf(field);
1909
- if (entry === void 0) return null;
1910
- if (entry.kind === "number") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NumberRow, {
1911
- t,
1912
- control: entry.control,
1913
- disabled: !writable,
1914
- onEdit: form.actions.edit,
1915
- onReset: form.actions.resetField
1916
- }, field.name);
1917
- if (entry.kind === "select") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SelectRow, {
1918
- t,
1919
- control: entry.control,
1920
- disabled: !writable,
1921
- open: openSelect === field.name,
1922
- onOpenChange,
1923
- onEdit: form.editSelect,
1924
- onReset: form.actions.resetField
1925
- }, field.name);
1926
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToggleRow, {
1927
- t,
1928
- control: entry.control,
1929
- disabled: !writable,
1930
- onToggle: form.onToggle,
1931
- onReset: form.actions.resetField
1932
- }, field.name);
2216
+ }), /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2217
+ className: "tps-card",
2218
+ children: group.fields.map((field) => {
2219
+ if (field.kind === "action") {
2220
+ const actionControl = {
2221
+ field: field.name,
2222
+ labelKey: field.labelKey,
2223
+ hintKey: field.hintKey,
2224
+ actionKey: field.actionKey ?? ""
2225
+ };
2226
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ActionRow, {
2227
+ t,
2228
+ control: actionControl,
2229
+ disabled: !writable,
2230
+ running: actionState[field.name] === "running",
2231
+ result: actionResults[field.name] ?? null,
2232
+ onRun: (_actionKey) => runAction(field)
2233
+ }, field.name);
2234
+ }
2235
+ const entry = controlOf(field);
2236
+ if (entry === void 0) return null;
2237
+ if (entry.kind === "number") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(NumberRow, {
2238
+ t,
2239
+ control: entry.control,
2240
+ disabled: !writable,
2241
+ onEdit: form.actions.edit,
2242
+ onReset: form.actions.resetField
2243
+ }, field.name);
2244
+ if (entry.kind === "select") return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(SelectRow, {
2245
+ t,
2246
+ control: entry.control,
2247
+ disabled: !writable,
2248
+ open: openSelect === field.name,
2249
+ onOpenChange,
2250
+ onEdit: form.editSelect,
2251
+ onReset: form.actions.resetField
2252
+ }, field.name);
2253
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ToggleRow, {
2254
+ t,
2255
+ control: entry.control,
2256
+ disabled: !writable,
2257
+ onToggle: form.onToggle,
2258
+ onReset: form.actions.resetField
2259
+ }, field.name);
2260
+ })
1933
2261
  })]
1934
2262
  }, group.group)) });
1935
2263
  }
@@ -1937,20 +2265,147 @@ window.__ModuleLoader__.load({
1937
2265
  function ToolPlusSection(props) {
1938
2266
  const { t, scope } = props;
1939
2267
  const form = useToolForm(scope, ALL_FIELDS);
2268
+ const sectionRef = (0, react.useRef)(null);
1940
2269
  const tabsId = (0, react.useRef)(`tps-${Math.random().toString(36).slice(2, 8)}`);
1941
2270
  const tabRefs = (0, react.useRef)([]);
1942
2271
  const [activeId, setActiveId] = (0, react.useState)();
1943
2272
  const [openSelect, setOpenSelect] = (0, react.useState)(null);
2273
+ const [actionState, setActionState] = (0, react.useState)({});
2274
+ const [actionResults, setActionResults] = (0, react.useState)({});
2275
+ const runAction = (0, react.useCallback)((field) => {
2276
+ const actionKey = field.actionKey;
2277
+ if (!actionKey) return;
2278
+ setActionState((prev) => ({
2279
+ ...prev,
2280
+ [field.name]: "running"
2281
+ }));
2282
+ setActionResults((prev) => ({
2283
+ ...prev,
2284
+ [field.name]: null
2285
+ }));
2286
+ (async () => {
2287
+ try {
2288
+ const result = await createWebConnectionRpc().call(TOOL_PLUS_RPC_CHANNEL, actionKey, {});
2289
+ if (!result.ok) setActionResults((prev) => ({
2290
+ ...prev,
2291
+ [field.name]: {
2292
+ ok: false,
2293
+ text: result.error.message
2294
+ }
2295
+ }));
2296
+ else {
2297
+ const found = result.value?.found ?? [];
2298
+ const text = found.length === 0 ? t("browserProbeNone") : found.map((browser) => `${browser.name}: ${browser.path}`).join("\n");
2299
+ setActionResults((prev) => ({
2300
+ ...prev,
2301
+ [field.name]: {
2302
+ ok: found.length > 0,
2303
+ text
2304
+ }
2305
+ }));
2306
+ }
2307
+ } catch {
2308
+ setActionResults((prev) => ({
2309
+ ...prev,
2310
+ [field.name]: {
2311
+ ok: false,
2312
+ text: t("browserProbeUnavailable")
2313
+ }
2314
+ }));
2315
+ } finally {
2316
+ setActionState((prev) => ({
2317
+ ...prev,
2318
+ [field.name]: "done"
2319
+ }));
2320
+ }
2321
+ })();
2322
+ }, [t]);
1944
2323
  (0, react.useEffect)(() => {
1945
2324
  injectSectionCss();
1946
2325
  injectSettingsRowsCss();
1947
2326
  }, []);
1948
- if (!form.shell.available) return null;
1949
2327
  const active = TOOL_TABS.find((row) => row.id === activeId)?.id ?? TOOL_TABS[0]?.id;
2328
+ const tabsRef = (0, react.useRef)(null);
2329
+ const indicatorRef = (0, react.useRef)(null);
2330
+ const committedRect = (0, react.useRef)(null);
2331
+ const commitIndicator = (0, react.useCallback)((animate) => {
2332
+ const tabs = tabsRef.current;
2333
+ const indicator = indicatorRef.current;
2334
+ if (tabs === null || indicator === null || active === void 0) return;
2335
+ const tab = tabs.querySelector(`[data-tps-tab="${active}"]`);
2336
+ if (tab === null) return;
2337
+ const left = tab.offsetLeft;
2338
+ const width = tab.offsetWidth;
2339
+ const previous = committedRect.current;
2340
+ committedRect.current = {
2341
+ left,
2342
+ width
2343
+ };
2344
+ if (previous !== null && previous.left === left && previous.width === width) return;
2345
+ const reduce = typeof window !== "undefined" && typeof window.matchMedia === "function" && window.matchMedia("(prefers-reduced-motion: reduce)").matches;
2346
+ if (!animate || reduce || previous === null) {
2347
+ indicator.style.transition = "none";
2348
+ indicator.style.width = `${width}px`;
2349
+ indicator.style.transform = `translateX(${left}px)`;
2350
+ return;
2351
+ }
2352
+ indicator.style.transition = "none";
2353
+ indicator.style.width = `${width}px`;
2354
+ indicator.style.transform = `translateX(${previous.left}px) scaleX(${previous.width / width})`;
2355
+ indicator.offsetWidth;
2356
+ indicator.style.transition = "transform .24s cubic-bezier(.22,1,.36,1)";
2357
+ indicator.style.transform = `translateX(${left}px) scaleX(1)`;
2358
+ }, [active]);
2359
+ (0, react.useLayoutEffect)(() => {
2360
+ commitIndicator(true);
2361
+ }, [commitIndicator]);
2362
+ const commitRef = (0, react.useRef)(commitIndicator);
2363
+ commitRef.current = commitIndicator;
2364
+ (0, react.useEffect)(() => {
2365
+ const tabs = tabsRef.current;
2366
+ if (tabs === null || typeof ResizeObserver === "undefined") return;
2367
+ const observer = new ResizeObserver(() => commitRef.current(false));
2368
+ observer.observe(tabs);
2369
+ for (const tab of Array.from(tabs.querySelectorAll("[data-tps-tab]"))) observer.observe(tab);
2370
+ return () => {
2371
+ observer.disconnect();
2372
+ };
2373
+ }, []);
2374
+ (0, react.useLayoutEffect)(() => {
2375
+ const root = sectionRef.current;
2376
+ if (root === null || typeof ResizeObserver === "undefined") return;
2377
+ const pane = scrollPaneOf(root);
2378
+ if (pane === null) return;
2379
+ let frame = 0;
2380
+ const measure = () => {
2381
+ frame = 0;
2382
+ const style = getComputedStyle(pane);
2383
+ const paddingTop = parseFloat(style.paddingTop) || 0;
2384
+ const contentHeight = pane.clientHeight - paddingTop - (parseFloat(style.paddingBottom) || 0);
2385
+ const offset = root.getBoundingClientRect().top - pane.getBoundingClientRect().top - (parseFloat(style.borderTopWidth) || 0) - paddingTop + pane.scrollTop;
2386
+ const next = `${Math.max(MIN_PAGE_HEIGHT, Math.round(contentHeight - offset))}px`;
2387
+ if (root.style.height !== next) root.style.height = next;
2388
+ };
2389
+ const schedule = () => {
2390
+ if (frame === 0) frame = requestAnimationFrame(measure);
2391
+ };
2392
+ measure();
2393
+ const observer = new ResizeObserver(schedule);
2394
+ observer.observe(pane);
2395
+ window.addEventListener("resize", schedule);
2396
+ return () => {
2397
+ if (frame !== 0) cancelAnimationFrame(frame);
2398
+ observer.disconnect();
2399
+ window.removeEventListener("resize", schedule);
2400
+ root.style.height = "";
2401
+ };
2402
+ }, [form.shell.available]);
2403
+ if (!form.shell.available) return null;
1950
2404
  const selectTab = (id) => setActiveId(id);
1951
2405
  const blocked = !form.shell.dirty || form.shell.invalid || form.shell.saving;
1952
2406
  const writable = form.shell.writable;
1953
2407
  return /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2408
+ ref: sectionRef,
1954
2409
  className: "tps-section",
1955
2410
  children: [
1956
2411
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h2", {
@@ -1961,11 +2416,16 @@ window.__ModuleLoader__.load({
1961
2416
  className: "tps-intro",
1962
2417
  children: t("pageDescription")
1963
2418
  }),
1964
- /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
2419
+ /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2420
+ ref: tabsRef,
1965
2421
  className: "tps-tabs",
1966
2422
  role: "tablist",
1967
2423
  "aria-label": t("pageTitle"),
1968
- children: TOOL_TABS.map((row, index) => {
2424
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2425
+ ref: indicatorRef,
2426
+ className: "tps-tabIndicator",
2427
+ "aria-hidden": "true"
2428
+ }), TOOL_TABS.map((row, index) => {
1969
2429
  const selected = row.id === active;
1970
2430
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
1971
2431
  ref: (element) => {
@@ -1978,6 +2438,7 @@ window.__ModuleLoader__.load({
1978
2438
  "aria-selected": selected,
1979
2439
  "aria-controls": `${tabsId.current}-panel-${row.id}`,
1980
2440
  "data-active": selected ? "true" : void 0,
2441
+ "data-tps-tab": row.id,
1981
2442
  tabIndex: selected ? 0 : -1,
1982
2443
  onClick: () => selectTab(row.id),
1983
2444
  onKeyDown: (event) => {
@@ -2005,7 +2466,7 @@ window.__ModuleLoader__.load({
2005
2466
  },
2006
2467
  children: t(row.labelKey)
2007
2468
  }, row.id);
2008
- })
2469
+ })]
2009
2470
  }),
2010
2471
  TOOL_TABS.filter((row) => row.id === active).map((row) => /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
2011
2472
  id: `${tabsId.current}-panel-${row.id}`,
@@ -2022,7 +2483,10 @@ window.__ModuleLoader__.load({
2022
2483
  form,
2023
2484
  writable,
2024
2485
  openSelect,
2025
- onOpenChange: setOpenSelect
2486
+ onOpenChange: setOpenSelect,
2487
+ actionState,
2488
+ actionResults,
2489
+ runAction
2026
2490
  })]
2027
2491
  }, row.id)),
2028
2492
  /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("div", {
@@ -2037,6 +2501,14 @@ window.__ModuleLoader__.load({
2037
2501
  className: "tps-applies",
2038
2502
  children: t("appliesTo")
2039
2503
  }) : null,
2504
+ !form.shell.failed && form.shell.dirty ? /* @__PURE__ */ (0, react_jsx_runtime.jsxs)("span", {
2505
+ className: "tps-unsaved",
2506
+ role: "status",
2507
+ children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
2508
+ className: "tps-unsavedDot",
2509
+ "aria-hidden": "true"
2510
+ }), t("unsaved")]
2511
+ }) : null,
2040
2512
  /* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
2041
2513
  type: "button",
2042
2514
  className: "tps-discard",
@@ -2104,6 +2576,23 @@ window.__ModuleLoader__.load({
2104
2576
  fetchEnabledHint: "read 处理 http(s) URL(网页转 Markdown 供模型阅读)时是否启用抓取。",
2105
2577
  fetchMaxTimeoutSeconds: "抓取超时(秒)",
2106
2578
  fetchMaxTimeoutSecondsHint: "URL 抓取超时上限;0 表示使用默认的 30 秒。",
2579
+ fetchReader: "网页转 Markdown 后端",
2580
+ fetchReaderHint: "优先使用的网页正文提取后端;auto 按内置顺序依次尝试(含浏览器 JS 渲染)。设为具体后端时,该后端优先,其余按原顺序兜底。",
2581
+ browserReaderEnabled: "浏览器 JS 渲染",
2582
+ browserReaderEnabledHint: "启用本地无头浏览器渲染(SPA 页面需执行 JavaScript 才能出正文)。未安装可用浏览器时自动跳过,不影响其他后端。",
2583
+ browserProbe: "探测浏览器",
2584
+ browserProbeHint: "检测本机可用的浏览器(Edge / Chrome / Chromium),read 的浏览器渲染将使用第一个命中的。",
2585
+ browserProbeRunning: "探测中…",
2586
+ browserProbeNone: "未找到可用浏览器:URL 渲染将跳过浏览器后端。",
2587
+ browserProbeFailed: "探测失败:",
2588
+ browserProbeUnavailable: "探测不可用:当前部署未启用浏览器 RPC 通道。",
2589
+ optReaderAuto: "auto(自动)",
2590
+ optReaderNative: "native(内置转换)",
2591
+ optReaderTrafilatura: "trafilatura",
2592
+ optReaderLynx: "lynx",
2593
+ optReaderParallel: "Parallel Reader",
2594
+ optReaderJina: "Jina Reader",
2595
+ optReaderBrowser: "browser(浏览器渲染)",
2107
2596
  groupFetch: "抓取",
2108
2597
  groupImages: "图片",
2109
2598
  imagesAutoResize: "图片自动缩放",
@@ -2258,6 +2747,23 @@ window.__ModuleLoader__.load({
2258
2747
  fetchEnabledHint: "Whether fetching is enabled when read handles http(s) URLs.",
2259
2748
  fetchMaxTimeoutSeconds: "Fetch timeout (s)",
2260
2749
  fetchMaxTimeoutSecondsHint: "URL fetch timeout cap; 0 = default 30s.",
2750
+ fetchReader: "HTML-to-Markdown backend",
2751
+ fetchReaderHint: "Preferred reader backend for extracting page bodies; auto tries the built-in order (including browser JS rendering). A concrete backend goes first, the rest fall back in order.",
2752
+ browserReaderEnabled: "Browser JS rendering",
2753
+ browserReaderEnabledHint: "Render pages in a local headless browser (SPA pages need JavaScript to produce a body). Skipped automatically when no usable browser is installed.",
2754
+ browserProbe: "Probe browsers",
2755
+ browserProbeHint: "Detect usable browsers on this machine (Edge / Chrome / Chromium); read’s browser rendering uses the first hit.",
2756
+ browserProbeRunning: "Probing…",
2757
+ browserProbeNone: "No usable browser found: URL rendering will skip the browser backend.",
2758
+ browserProbeFailed: "Probe failed: ",
2759
+ browserProbeUnavailable: "Probing unavailable: this deployment does not enable the browser RPC channel.",
2760
+ optReaderAuto: "auto",
2761
+ optReaderNative: "native (built-in conversion)",
2762
+ optReaderTrafilatura: "trafilatura",
2763
+ optReaderLynx: "lynx",
2764
+ optReaderParallel: "Parallel Reader",
2765
+ optReaderJina: "Jina Reader",
2766
+ optReaderBrowser: "browser (JS rendering)",
2261
2767
  groupFetch: "Fetching",
2262
2768
  groupImages: "Images",
2263
2769
  imagesAutoResize: "Auto-resize images",