@nimoxie/dsh-ui-tweaks 0.1.7 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/client.js CHANGED
@@ -14,7 +14,7 @@ window.__ModuleLoader__.load({
14
14
  const module = { exports: {} };
15
15
  const exports = module.exports;
16
16
 
17
- const BUILD = "0.1.7";
17
+ const BUILD = "0.2.0";
18
18
 
19
19
  /* 每个模块的注册各套一层 guard:失败只记 warn(座位/模块失败模式是「静默消失」,
20
20
  必须主动吭声;但不能让一个模块的异常打断同文件里其它模块的 apply)。 */
@@ -390,6 +390,19 @@ window.__ModuleLoader__.load({
390
390
  var BTN_ID = "dshuiw-updates-btn-host";
391
391
  var RBTN_ID = "dshuiw-restart-btn-host";
392
392
  var PANEL_ID = "dshuiw-updates-panel-host";
393
+ /* 用户是否主动收起了面板。paint() 只负责重画,**不负责把面板弹出来** ——
394
+ 否则后台一有动静(装完一个包、冷静期到点自动重查、超时兜底)就会把用户
395
+ 刚收起来的面板又弹回来。显示只由「打开」路径决定,隐藏只由「收起」路径决定。 */
396
+ var panelClosed = false;
397
+ /* 弹窗里的控件都是自绘 span(渲染结构与 CSS 都不动),键盘得自己接:
398
+ 可点的补 role=button/tabindex=0(KBD),Enter/Space 派发一次 click,
399
+ 走同一套点击委托,行为与鼠标完全一致。 */
400
+ var KBD = ' role="button" tabindex="0"';
401
+ /* 焦点环用官方自己的令牌(app 里有 237 条 :focus-visible,主题变量就是
402
+ --dsw-focus-ring-*),与原生控件的焦点样式同色同宽,不自造灰色。
403
+ 三处使用点:两个 shadow 样式 + 页面级那两枚按钮。 */
404
+ var FOCUS_RING = "outline:var(--dsw-focus-ring-width,2px) solid " +
405
+ "var(--dsw-focus-ring-color,var(--dsw-alias-state-business-primary,rgba(120,170,255,.9)));outline-offset:2px";
393
406
 
394
407
  /* ---- semver 比较(rc 比数字/字母、正式版 > 预发布版;字符串比会把
395
408
  rc.9 判成大于 rc.10,必须自己实现)---- */
@@ -738,6 +751,7 @@ window.__ModuleLoader__.load({
738
751
  **先问会话有没有在跑** —— 没有在跑的会话就直接执行,不打扰人;有才弹中断提示。
739
752
  宿主形态决定重启怎么做(shell / relaunch / manual),文案按形态给。 */
740
753
  var RST_PANEL_ID = 'dshuiw-restart-panel-host';
754
+ var scrimShown = false; // 蒙皮是否正盖在页面上(键盘滚动锁的开关)
741
755
  var rstStage = ''; // '' | checking | auto | confirm | busy | done | error
742
756
  var rstMode = ''; // relaunch | shell | manual
743
757
  var rstNote = '';
@@ -826,6 +840,7 @@ window.__ModuleLoader__.load({
826
840
  'background:var(--dsw-alias-bg-base,#1b1d23);color:var(--dsw-alias-label-primary,#e8e9ec);' +
827
841
  'box-shadow:0 10px 30px rgba(0,0,0,.34);font:13px/1.55 system-ui,-apple-system,"Microsoft YaHei UI",sans-serif}' +
828
842
  '.rwrap[hidden]{display:none}' +
843
+ '[role="button"]:focus-visible{' + FOCUS_RING + '}' +
829
844
  '.rt{font-weight:600;margin-bottom:6px}' +
830
845
  '.rb{font-size:12px;line-height:1.5;opacity:.85;white-space:pre-wrap;word-break:break-word}' +
831
846
  '.rl{margin:6px 0 0 10px;font-size:12px;opacity:.9}' +
@@ -840,7 +855,7 @@ window.__ModuleLoader__.load({
840
855
  '.rscrim{position:fixed;top:0;left:0;right:0;bottom:0;z-index:2147483000;cursor:progress;' +
841
856
  'background:rgba(8,10,14,.5);backdrop-filter:blur(1.5px);-webkit-backdrop-filter:blur(1.5px)}' +
842
857
  '.rscrim[hidden]{display:none}' +
843
- '</style><div class="rscrim" hidden></div><div class="rwrap" hidden></div>';
858
+ '</style><div class="rscrim" hidden></div><div class="rwrap" role="dialog" aria-modal="true" aria-label="重启 dsh" hidden></div>';
844
859
  var scrim = sr.querySelector('.rscrim');
845
860
  try {
846
861
  scrim.addEventListener('wheel', function (e) { e.preventDefault(); }, { passive: false });
@@ -852,7 +867,7 @@ window.__ModuleLoader__.load({
852
867
  if (!t || !t.classList) { return; }
853
868
  if (t.classList.contains('rgo')) { restartGo(); return; }
854
869
  if (t.classList.contains('rretry')) { restartOpen(); return; }
855
- if (t.classList.contains('rcancel') || t.classList.contains('rdismiss')) { restartClose(); return; }
870
+ if (t.classList.contains('rcancel') || t.classList.contains('rdismiss')) { restartClose(true); return; }
856
871
  });
857
872
  /* 点外部/Esc 收起;点自己那枚按钮不算外部(交给按钮做开关) */
858
873
  document.addEventListener('pointerdown', function (e) {
@@ -865,18 +880,46 @@ window.__ModuleLoader__.load({
865
880
  document.addEventListener('keydown', function (e) {
866
881
  if (e.key !== 'Escape' || rstStage === 'busy') { return; }
867
882
  var cur = host.shadowRoot.querySelector('.rwrap');
868
- if (cur && !cur.hidden) { restartClose(); }
883
+ if (cur && !cur.hidden) { restartClose(true); }
869
884
  });
885
+ /* 蒙皮期间页面已 inert,但键盘仍可能滚动后面的文档(PageDown/空格/方向键):
886
+ 在捕获段拦掉纯滚动键。焦点在弹窗里时 e.target 会被重定向成宿主,那时不拦 ——
887
+ 弹窗该能自己滚,按钮也该能用空格触发。 */
888
+ document.addEventListener('keydown', function (e) {
889
+ if (!scrimShown) { return; }
890
+ var hostEl = document.getElementById(RST_PANEL_ID);
891
+ if (hostEl && (e.target === hostEl || hostEl.contains(e.target))) { return; }
892
+ var k = e.key;
893
+ if (k === 'PageDown' || k === 'PageUp' || k === 'ArrowDown' || k === 'ArrowUp' ||
894
+ k === 'Home' || k === 'End' || k === ' ' || k === 'Spacebar') {
895
+ e.preventDefault();
896
+ }
897
+ }, true);
898
+ wireKeyActivation(wrap);
870
899
  return wrap;
871
900
  }
872
901
 
873
902
  /* ---- 蒙皮:确认重启后把整页蒙住(客户端此时已不可用,界面元素不该还能点)---- */
903
+ /* 蒙皮只挡指针。键盘仍能 Tab 到后面的元素并回车激活,所以同时把 body 的其它
904
+ 子树整块 inert 掉(Chromium 102+ 支持;弹窗自己的宿主除外)。关掉时如实还原。 */
905
+ function setPageInert(on) {
906
+ try {
907
+ var kids = document.body.children;
908
+ for (var i = 0; i < kids.length; i++) {
909
+ var el = kids[i];
910
+ if (!el || el.id === RST_PANEL_ID) { continue; }
911
+ if (on) { el.setAttribute("inert", ""); } else { el.removeAttribute("inert"); }
912
+ }
913
+ } catch (e) {}
914
+ }
874
915
  function showScrim(on) {
875
916
  try {
876
917
  rstWrap(); // 确保阴影根在(隐藏的弹窗也没关系)
877
918
  var host = document.getElementById(RST_PANEL_ID);
878
919
  var s = host && host.shadowRoot ? host.shadowRoot.querySelector('.rscrim') : null;
879
920
  if (s) { s.hidden = !on; }
921
+ scrimShown = !!on;
922
+ setPageInert(on);
880
923
  } catch (e) {}
881
924
  }
882
925
  var rstKillTimer = 0;
@@ -899,7 +942,7 @@ window.__ModuleLoader__.load({
899
942
 
900
943
  function rstRender() {
901
944
  var wrap = rstWrap();
902
- function btn(cls, label) { return '<span class="rbtn ' + cls + '">' + label + '</span>'; }
945
+ function btn(cls, label) { return '<span class="rbtn ' + cls + '"' + KBD + '>' + label + '</span>'; }
903
946
  var title = '重启 dsh';
904
947
  var body = '';
905
948
  var actions = '';
@@ -939,14 +982,25 @@ window.__ModuleLoader__.load({
939
982
  if (wrap.innerHTML !== html) { wrap.innerHTML = html; }
940
983
  wrap.hidden = false;
941
984
  placeNear(rstBtn, wrap, RST_WIDTH, 180);
985
+ /* 键盘:确认/失败/收尾态把焦点交给主按钮;弹窗里已经有焦点就不抢 */
986
+ if (rstStage === 'confirm' || rstStage === 'error' || rstStage === 'done') {
987
+ try {
988
+ var hostEl = document.getElementById(RST_PANEL_ID);
989
+ var inside = hostEl && hostEl.shadowRoot ? hostEl.shadowRoot.activeElement : null;
990
+ var primary = wrap.querySelector('.rgo, .rretry, .rdismiss, .rcancel');
991
+ if (primary && !inside) { primary.focus(); }
992
+ } catch (e) {}
993
+ }
942
994
  }
943
995
 
944
- function restartClose() {
996
+ function restartClose(refocus) {
945
997
  showScrim(false);
946
998
  clearKillTimer();
947
999
  var wrap = rstWrap();
948
1000
  wrap.hidden = true;
949
1001
  rstStage = ''; rstNote = ''; rstActive = null; rstNames = [];
1002
+ /* 键盘:收起后把焦点还给「立即重启」。点外部收起的传 false */
1003
+ if (refocus) { try { if (rstBtn && document.contains(rstBtn)) { rstBtn.focus(); } } catch (e) {} }
950
1004
  }
951
1005
 
952
1006
  /* 打开:并行读「重启方案」与「在跑的会话」,两者都回来再决定 */
@@ -1049,7 +1103,7 @@ window.__ModuleLoader__.load({
1049
1103
  var stage = forceStage[name] || "";
1050
1104
  var ver = r.latest || r.version;
1051
1105
  function btn(cls, label) {
1052
- return "<span class=\"fbtn " + cls + "\" data-n=\"" + esc(name) + "\">" + label + "</span>";
1106
+ return "<span class=\"fbtn " + cls + "\" data-n=\"" + esc(name) + "\"" + KBD + ">" + label + "</span>";
1053
1107
  }
1054
1108
  if (stage === "checking") { return "<div class=\"force\"><div class=\"fn\">正在预检…</div></div>"; }
1055
1109
  if (stage === "writing") { return "<div class=\"force\"><div class=\"fn\">正在写入豁免表…</div></div>"; }
@@ -1589,7 +1643,26 @@ window.__ModuleLoader__.load({
1589
1643
  来源:触发它的那枚按钮。按钮被页面布局推走(滚动、窗口尺寸变化、插件页重排)
1590
1644
  时弹窗跟着走,不会有"拖到哪就钉在哪、之后再也不跟"的状态。 */
1591
1645
  var PANEL_GAP = 6, PANEL_MARGIN = 8;
1646
+ /* 兜底宽度:只在弹窗还没渲染、量不到布局时用 */
1592
1647
  var PANEL_WIDTH = 344, RST_WIDTH = 330;
1648
+ /* 量真实外框。调用方传的 width 只是 CSS 里的**内容宽**,而两个弹窗的盒模型并不一样:
1649
+ Shadow DOM 里拿不到官方全局的 box-sizing 复位,.rwrap 是 content-box 且带 12px
1650
+ padding → 实宽比常量多 26px,右缘会比按钮多探出 26px(.wrap 没有 padding,
1651
+ 只多 2px 边框,所以看着是对齐的)。竖直方向同理:maxHeight 约束的是内容盒,
1652
+ 不扣掉 padding+border,弹窗就会比算出来的空间高出 26px。两边都用实测值。 */
1653
+ function measuredBox(wrap, fallbackWidth) {
1654
+ /* 宽度用 getBoundingClientRect:和锚点矩形同一个坐标系,页面若带缩放也不会串 */
1655
+ var width = 0;
1656
+ try { width = wrap.getBoundingClientRect().width; } catch (e) {}
1657
+ if (!width) { width = wrap.offsetWidth || fallbackWidth; }
1658
+ var extraY = 0;
1659
+ try {
1660
+ var cs = window.getComputedStyle(wrap);
1661
+ var n = function (v) { var f = parseFloat(v); return isFinite(f) ? f : 0; };
1662
+ extraY = n(cs.paddingTop) + n(cs.paddingBottom) + n(cs.borderTopWidth) + n(cs.borderBottomWidth);
1663
+ } catch (e) {}
1664
+ return { width: width, extraY: extraY };
1665
+ }
1593
1666
  function placeNear(anchor, wrap, width, minHeight) {
1594
1667
  try {
1595
1668
  if (!wrap || wrap.hidden) { return; }
@@ -1597,14 +1670,20 @@ window.__ModuleLoader__.load({
1597
1670
  var r = null;
1598
1671
  if (anchor && anchor.getBoundingClientRect) { r = anchor.getBoundingClientRect(); }
1599
1672
  if (!r || (!r.width && !r.height)) { r = { right: vw - 16, top: 56, bottom: 56 }; } // 按钮还没挂上:退回右上角
1600
- var left = Math.max(PANEL_MARGIN, Math.min(vw - width - PANEL_MARGIN, r.right - width));
1601
- var height = wrap.offsetHeight || minHeight || 200;
1673
+ var box = measuredBox(wrap, width);
1674
+ var outer = box.width;
1675
+ var left = Math.max(PANEL_MARGIN, Math.min(vw - outer - PANEL_MARGIN, r.right - outer));
1676
+ /* scrollHeight 是未被 maxHeight 夹过的自然高度:反复重排时判据不会漂 */
1677
+ var height = wrap.scrollHeight || wrap.offsetHeight || minHeight || 200;
1602
1678
  var below = vh - r.bottom - PANEL_GAP - PANEL_MARGIN;
1603
1679
  var above = r.top - PANEL_GAP - PANEL_MARGIN;
1604
1680
  /* 优先放下方;下方放不下(或不如上方宽裕)就翻到上方 */
1605
1681
  var useBelow = below >= Math.min(height, minHeight || 200) || below >= above;
1606
- var room = Math.max(120, useBelow ? below : above);
1607
- wrap.style.maxHeight = room + 'px';
1682
+ /* 不再把 room 强行抬到 120:按钮贴着视口上下边时那会突破边距,
1683
+ 让弹窗(连同底部的「重启 dsh / 取消」)跑到视口外面去。
1684
+ 真正的下限交给下面 maxHeight 的 60。 */
1685
+ var room = useBelow ? below : above;
1686
+ wrap.style.maxHeight = Math.max(60, room - box.extraY) + 'px';
1608
1687
  wrap.style.left = left + 'px';
1609
1688
  wrap.style.right = 'auto';
1610
1689
  wrap.style.top = (useBelow
@@ -1612,6 +1691,24 @@ window.__ModuleLoader__.load({
1612
1691
  : Math.max(PANEL_MARGIN, r.top - PANEL_GAP - Math.min(height, room))) + 'px';
1613
1692
  } catch (e) {}
1614
1693
  }
1694
+ function wireKeyActivation(wrap) {
1695
+ wrap.addEventListener("keydown", function (e) {
1696
+ if (e.key !== "Enter" && e.key !== " " && e.key !== "Spacebar") { return; }
1697
+ var t = e.target;
1698
+ if (!t || !t.getAttribute || t.getAttribute("role") !== "button") { return; }
1699
+ e.preventDefault(); // Space 默认会滚动页面
1700
+ try { t.click(); } catch (e2) {}
1701
+ });
1702
+ }
1703
+ /* 收起更新面板:置位 + 隐藏,状态不动(后台的安装/检查继续跑) */
1704
+ function closePanel(refocus) {
1705
+ panelClosed = true;
1706
+ var w = liveWrap(PANEL_ID, ".wrap");
1707
+ if (w) { w.hidden = true; }
1708
+ /* 键盘:收起后把焦点还给触发它的那枚按钮。点外部收起的传 false ——
1709
+ 焦点已经跟着用户点的地方去了,不该抢回来。 */
1710
+ if (refocus) { try { var b = document.getElementById(BTN_ID); if (b) { b.focus(); } } catch (e) {} }
1711
+ }
1615
1712
  /* 已经建出来且开着的弹窗;重排时不要顺手把它们创建出来 */
1616
1713
  function liveWrap(hostId, sel) {
1617
1714
  var host = document.getElementById(hostId);
@@ -1635,7 +1732,17 @@ window.__ModuleLoader__.load({
1635
1732
  }
1636
1733
  try {
1637
1734
  window.addEventListener('resize', relayoutPanels);
1638
- window.addEventListener('scroll', relayoutPanels, true);
1735
+ /* 面板自己内部滚动(.rows)不影响按钮位置,不必重排;用 composedPath 认出来 */
1736
+ window.addEventListener('scroll', function (e) {
1737
+ if (e && typeof e.composedPath === 'function') {
1738
+ var path = e.composedPath();
1739
+ for (var i = 0; i < path.length; i++) {
1740
+ var n = path[i];
1741
+ if (n && n.id && (n.id === PANEL_ID || n.id === RST_PANEL_ID)) { return; }
1742
+ }
1743
+ }
1744
+ relayoutPanels();
1745
+ }, true);
1639
1746
  } catch (e) {}
1640
1747
 
1641
1748
  /* ---- 面板 UI:挂在 body 的 Shadow DOM(fixed 浮层不能放进 flex 工具栏)---- */
@@ -1655,6 +1762,7 @@ window.__ModuleLoader__.load({
1655
1762
  "box-shadow:0 10px 30px rgba(0,0,0,.34);" +
1656
1763
  "font:13px/1.55 system-ui,-apple-system,\"Microsoft YaHei UI\",sans-serif}" +
1657
1764
  ".wrap[hidden]{display:none}" +
1765
+ "[role=\"button\"]:focus-visible{" + FOCUS_RING + "}" +
1658
1766
  ".hd{display:flex;align-items:center;gap:8px;padding:10px 12px;flex:0 0 auto;" +
1659
1767
  "border-bottom:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.22));" +
1660
1768
  "user-select:none;-webkit-user-select:none}" +
@@ -1712,7 +1820,7 @@ window.__ModuleLoader__.load({
1712
1820
  "border-bottom:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.18))}" +
1713
1821
  ".foot{padding:8px 12px;font-size:11px;line-height:1.5;opacity:.6;" +
1714
1822
  "border-top:1px solid var(--dsw-alias-border-l2,rgba(128,128,128,.12))}" +
1715
- "</style><div class=\"wrap\" hidden></div>";
1823
+ "</style><div class=\"wrap\" role=\"dialog\" aria-label=\"插件更新\" tabindex=\"-1\" hidden></div>";
1716
1824
  var wrap = sr.querySelector(".wrap");
1717
1825
 
1718
1826
  /* 不可拖动:位置完全由 placeNear() 决定(见文件上方的「弹窗定位」)。
@@ -1726,19 +1834,21 @@ window.__ModuleLoader__.load({
1726
1834
  var cur = host.shadowRoot.querySelector(".wrap");
1727
1835
  if (!cur || cur.hidden) { return; }
1728
1836
  if (host.contains(e.target)) { return; }
1729
- cur.hidden = true;
1837
+ var chkBtn = document.getElementById(BTN_ID);
1838
+ if (chkBtn && (chkBtn === e.target || chkBtn.contains(e.target))) { return; }
1839
+ closePanel();
1730
1840
  }, true);
1731
1841
  document.addEventListener("keydown", function (e) {
1732
1842
  if (e.key !== "Escape") { return; }
1733
1843
  var cur = host.shadowRoot.querySelector(".wrap");
1734
- if (cur) { cur.hidden = true; }
1844
+ if (cur && !cur.hidden) { closePanel(true); }
1735
1845
  });
1736
1846
 
1737
1847
  /* 行内点击委托:有新版本 → 入队;更新中/等待 → 取消;失败 → 重试 */
1738
1848
  wrap.addEventListener("click", function (e) {
1739
1849
  var t = e.target;
1740
1850
  if (!t || !t.classList) { return; }
1741
- if (t.classList.contains("x")) { wrap.hidden = true; return; }
1851
+ if (t.classList.contains("x")) { closePanel(true); return; }
1742
1852
  if (t.classList.contains("recheck")) { if (!checking) { checkNow(); } return; }
1743
1853
  if (t.classList.contains("reverify")) { if (!checking) { checkNow(); } return; }
1744
1854
  if (t.classList.contains("allbtn")) {
@@ -1783,6 +1893,8 @@ window.__ModuleLoader__.load({
1783
1893
  }
1784
1894
  });
1785
1895
 
1896
+ wireKeyActivation(wrap);
1897
+
1786
1898
  /* 状态 → 行渲染订阅 */
1787
1899
  listeners.push(function () { paint(); });
1788
1900
  return wrap;
@@ -1835,12 +1947,19 @@ window.__ModuleLoader__.load({
1835
1947
  else if (r.state === "fail") { cls = "tag error act clickable"; act = " data-n=\"" + esc(r.name) + "\""; title = " title=\"" + esc(r.why || "更新失败") + "(点击重试)\""; }
1836
1948
  else if (r.state === "unchanged") { cls = "tag update act clickable"; act = " data-n=\"" + esc(r.name) + "\""; title = " title=\"" + esc(r.why || "") + "(点击重试)\""; }
1837
1949
  else if (r.state === "unconfirmed") { cls = "tag other reverify clickable"; title = " title=\"" + esc(r.why || "") + "(点击重查)\""; }
1838
- return "<span class=\"" + cls + "\"" + act + title + ">" + esc(TAG_TEXT[r.state] || "未比对") + "</span>";
1839
- }
1840
- /* ---- 「Auto review」访问模式提示(只提示,不改任何设置)--------------------
1841
- DSH 0.2.0-rc.2 的已知缺陷:访问模式为 auto 的会话会让 permissionPresets 服务在
1842
- 热重载(装/更新插件就会触发)时构造失败 —— 表现是装插件报一长串错、助手侧也随之
1843
- 一连串报错,且要重启客户端才恢复。这里只读当前是否有这种会话,用来提醒用户先切走。
1950
+ return "<span class=\"" + cls + "\"" + act + title + (cls.indexOf("clickable") >= 0 ? KBD : "") + ">" + esc(TAG_TEXT[r.state] || "未比对") + "</span>";
1951
+ }
1952
+ /* ---- 「自动审查」访问模式提示(只提示,不改任何设置)--------------------
1953
+ 两件事都跟这个模式有关,所以合成一条提醒:
1954
+ ① DSH 0.2.0-rc.2 的已知缺陷:访问模式为 auto 的会话会让 permissionPresets 服务在
1955
+ 热重载(装/更新插件就会触发)时构造失败 —— 表现是装插件报一长串错、助手侧也随之
1956
+ 一连串报错,且要重启客户端才恢复。
1957
+ ② 该模式对每次工具调用还有一道「调用前审查」,审查服务不可用或限流时这次调用会被
1958
+ 直接拒绝、工具体不执行(文案形如 Auto review of tool "…" failed)。
1959
+ 0.2.1-rc.2 复核(2026-10-18):预设标识仍是 AUTO_PRESET = "auto"(界面改叫
1960
+ 「自动审查」,判定串 currentValue === "auto" 不受影响);旧缺陷的 throw 仍在,
1961
+ 但 auto-review 集成新增了卸载时的会话迁移(先把 auto 会话改成 danger-full-access
1962
+ 再退出),触发窗口已收窄。这里只读当前是否有这种会话,用来提醒用户先切走。
1844
1963
  读法同官方权限面板:sessions 列表 → binding(id).session → permissions 投影。
1845
1964
  拿不到服务/投影(老版本 dsh、会话未驻留)就返回 false,宁可不提示也不误报。 */
1846
1965
  function hasAutoReviewSession() {
@@ -1865,7 +1984,7 @@ window.__ModuleLoader__.load({
1865
1984
  }
1866
1985
  function accessModeBar() {
1867
1986
  if (!hasAutoReviewSession()) { return ""; }
1868
- return "<div class=\"warn\">⚠ 有会话正在用「Auto review」访问模式:这个模式下装/更新插件有时会失败,并且助手这边会开始一连串报错,要重启客户端才能恢复。建议先把该会话切换成「工作区内修改」,装完再切回「Auto review」。</div>";
1987
+ return "<div class=\"warn\">⚠ 有会话正在用「自动审查」(Auto review)访问模式:这个模式下装/更新插件有时会失败,助手这边也会跟着一连串报错,要重启客户端才能恢复;该模式下每次工具调用还会先过一遍审查,审查服务不可用或限流时,这次调用会被直接拒绝、不执行。建议先把该会话切换成「工作区内修改」,装完再切回「自动审查」。</div>";
1869
1988
  }
1870
1989
 
1871
1990
  function paint() {
@@ -1879,7 +1998,7 @@ window.__ModuleLoader__.load({
1879
1998
  (isDesktop ? "重启客户端后生效" : "重启 dsh 服务并刷新页面后生效") + "</div>";
1880
1999
  }
1881
2000
  rows.forEach(function (r) { if (r.state === "update") { upd++; } });
1882
- var allBtn = upd ? "<span class=\"allbtn\">全部更新(" + upd + ")</span>" : "<span class=\"allbtn\" hidden></span>";
2001
+ var allBtn = upd ? "<span class=\"allbtn\"" + KBD + ">全部更新(" + upd + ")</span>" : "<span class=\"allbtn\" hidden></span>";
1883
2002
  var body;
1884
2003
  if (!rows.length) {
1885
2004
  body = "<div class=\"msg\">" + (checking ? "正在读取插件清单…" : "点上方「重新检查」开始比对") + "</div>";
@@ -1903,8 +2022,8 @@ window.__ModuleLoader__.load({
1903
2022
  }).join("");
1904
2023
  }
1905
2024
  var html = "<div class=\"hd\"><b>插件更新 · " + (busyName ? "正在更新 " + esc(busyName) + " …" : (checking ? "检查中…" : (rows.length ? upd + " 个插件有新版本" : "插件更新检查"))) + "</b>" +
1906
- "<span class=\"recheck\" title=\"重新读取插件清单并比对 registry\">重新检查</span>" + allBtn +
1907
- "<span class=\"x\" title=\"关闭\">×</span></div>" + restartBar() + accessModeBar() + "<div class=\"rows\">" + body + "</div>" +
2025
+ "<span class=\"recheck\"" + KBD + " title=\"重新读取插件清单并比对 registry\">重新检查</span>" + allBtn +
2026
+ "<span class=\"x\"" + KBD + " title=\"关闭\" aria-label=\"关闭\">×</span></div>" + restartBar() + accessModeBar() + "<div class=\"rows\">" + body + "</div>" +
1908
2027
  /* 面板脚注:把「只查第三方」这条口径写在明处(plugin-manager 同款一句) */
1909
2028
  "<div class=\"foot\">只检查第三方插件;官方 @deepseek-ai/ 包随 DSH 本体升级。</div>";
1910
2029
  if (wrap.innerHTML !== html) {
@@ -1914,9 +2033,12 @@ window.__ModuleLoader__.load({
1914
2033
  var nk = wrap.querySelector(".rows");
1915
2034
  if (nk && st) { nk.scrollTop = st; }
1916
2035
  }
1917
- wrap.hidden = false;
1918
- /* 位置只有一个来源:那枚「检查更新」按钮(不可拖动) */
1919
- placeNear(document.getElementById(BTN_ID), wrap, PANEL_WIDTH, 220);
2036
+ /* 收起状态下只更新内容,不把它弹回来 —— 显示由「打开」路径负责 */
2037
+ if (!panelClosed) {
2038
+ wrap.hidden = false;
2039
+ /* 位置只有一个来源:那枚「检查更新」按钮(不可拖动) */
2040
+ placeNear(document.getElementById(BTN_ID), wrap, PANEL_WIDTH, 220);
2041
+ }
1920
2042
  }
1921
2043
 
1922
2044
  /* ---- 列表级按钮:挂官方插件页「已安装」标题那一行的右侧 -----------------
@@ -1940,6 +2062,9 @@ window.__ModuleLoader__.load({
1940
2062
  'color:var(--dsw-alias-label-primary,inherit);background:transparent;white-space:nowrap}' +
1941
2063
  '#' + BOX_ID + ' button:hover:enabled{background:var(--dsw-alias-interactive-bg-hover,rgba(128,128,128,.14))}' +
1942
2064
  '#' + BOX_ID + ' button:disabled{cursor:default;opacity:.55}' +
2065
+ /* 焦点环:这两枚是页面里自绘的 <button>,官方全局样式不会替它们画焦点,
2066
+ 键盘用户 Tab 过来时看不见落点(面板内部同样有一条,见两处 shadow 样式) */
2067
+ '#' + BOX_ID + ' button:focus-visible{' + FOCUS_RING + '}' +
1943
2068
  /* 「立即重启」用危险色:它是本插件里唯一会动进程、会打断进行中任务的按钮 */
1944
2069
  '#' + RBTN_ID + '{color:var(--dsw-alias-label-danger,#e07070);box-shadow:inset 0 0 0 1px rgba(255,80,80,.5)}' +
1945
2070
  '#' + RBTN_ID + ':hover:enabled{background:rgba(255,80,80,.12)}';
@@ -1987,7 +2112,7 @@ window.__ModuleLoader__.load({
1987
2112
  }
1988
2113
 
1989
2114
  mountButton(
1990
- function () { paint(); if (!rows.length && !checking) { checkNow(); } },
2115
+ function () { panelClosed = false; paint(); if (!rows.length && !checking) { checkNow(); } },
1991
2116
  function () { onRestartClick(); }
1992
2117
  );
1993
2118
  }
@@ -2077,12 +2202,305 @@ window.__ModuleLoader__.load({
2077
2202
  } catch (e) {}
2078
2203
  }
2079
2204
 
2205
+ /* ======================================================================
2206
+ * 模块 5:回合尾「本回合改动」补位卡(0.2.0 起)
2207
+ *
2208
+ * 缺陷(0.2.1-rc.2 实测):官方回合尾卡读的是 workspaceChanges 服务,摘要与
2209
+ * 对比只在内存 + 系统临时目录里活着,Session 一释放就 404,卡片变成一句
2210
+ * 「这轮改动的内容已不可用」。但每次文件工具调用都把前后全文写在工具结果事件的
2211
+ * meta.diffs: [{path, oldText, newText}] 里,**随会话日志永久保存** —— 客户端画
2212
+ * 工具卡 diff 用的就是它(dsh-client-ui-tool 的 appliedDiffs)。所以本模块
2213
+ * **不新增任何持久化**:用 uiConversation 的 Turn Definition 把一回合内这些
2214
+ * meta.diffs 聚合起来,注册到 conversation.chat.turnTail(官方声明的 list 槽位,
2215
+ * 已有 deliverables / plan / schedule 三家),并且**只在官方卡已经失效时才补位**。
2216
+ *
2217
+ * 边界(README 同步):
2218
+ * - 只覆盖**文件工具**的改动;pwsh / npm 等造成的改动进不来(只有官方卡含)。
2219
+ * - 嵌套调用(run_code 里的 edit)走 tool/ptc-dispatch 事件,该事件没有 turn 字段,
2220
+ * 用 rootCallId → turn 的映射(来自 tool/call)挂回回合。
2221
+ * - 探测结果只缓存在本次连接内(connection/reset 清空)。官方自己永久缓存 missing,
2222
+ * 那会在宿主恢复后留下双份卡。
2223
+ * ====================================================================== */
2224
+ var REACT = null;
2225
+ try { REACT = require("react"); } catch (e) { REACT = null; }
2226
+
2227
+ var CHANGES_KIND = "ui-tweaks-changes";
2228
+ var SUMMARY_ROUTE = "api/changes.summary"; /* 文档相对,勿加前导斜杠(同官方 CHANGED_FILES_ROUTE) */
2229
+ var DIFF_ROW_LIMIT = 400; /* 单个文件最多画多少行 diff */
2230
+ var LCS_LINE_LIMIT = 1200; /* 超过就退化为「全删 + 全增」,避免 DP 吃内存 */
2231
+ var probeCache = {}; /* sessionId:seq -> "ok" | "missing" */
2232
+ var rootTurn = {}; /* rootCallId -> turn(ptc 事件没有 turn) */
2233
+
2234
+ function splitLines(text) {
2235
+ var s = text == null ? "" : String(text);
2236
+ if (s === "") { return []; }
2237
+ if (s.charAt(s.length - 1) === "\n") { s = s.slice(0, -1); }
2238
+ return s.split("\n");
2239
+ }
2240
+ function naiveRows(a, b) {
2241
+ var rows = [], i;
2242
+ for (i = 0; i < a.length; i++) { rows.push({ k: "-", t: a[i] }); }
2243
+ for (i = 0; i < b.length; i++) { rows.push({ k: "+", t: b[i] }); }
2244
+ return rows;
2245
+ }
2246
+ /* 行级 LCS diff;超限或异常一律退化为「全删+全增」,绝不抛。 */
2247
+ function diffRows(oldText, newText) {
2248
+ var a = splitLines(oldText), b = splitLines(newText);
2249
+ if (a.length > LCS_LINE_LIMIT || b.length > LCS_LINE_LIMIT) { return naiveRows(a, b); }
2250
+ var n = a.length, m = b.length, w = m + 1;
2251
+ var dp;
2252
+ try { dp = new Int32Array((n + 1) * w); } catch (e) { return naiveRows(a, b); }
2253
+ for (var i = n - 1; i >= 0; i--) {
2254
+ for (var j = m - 1; j >= 0; j--) {
2255
+ dp[i * w + j] = a[i] === b[j]
2256
+ ? dp[(i + 1) * w + (j + 1)] + 1
2257
+ : Math.max(dp[(i + 1) * w + j], dp[i * w + (j + 1)]);
2258
+ }
2259
+ }
2260
+ var rows = [], x = 0, y = 0;
2261
+ while (x < n && y < m) {
2262
+ if (a[x] === b[y]) { rows.push({ k: " ", t: a[x] }); x++; y++; }
2263
+ else if (dp[(x + 1) * w + y] >= dp[x * w + (y + 1)]) { rows.push({ k: "-", t: a[x] }); x++; }
2264
+ else { rows.push({ k: "+", t: b[y] }); y++; }
2265
+ }
2266
+ while (x < n) { rows.push({ k: "-", t: a[x] }); x++; }
2267
+ while (y < m) { rows.push({ k: "+", t: b[y] }); y++; }
2268
+ return rows;
2269
+ }
2270
+ function countRows(rows) {
2271
+ var added = 0, deleted = 0;
2272
+ for (var i = 0; i < rows.length; i++) {
2273
+ if (rows[i].k === "+") { added++; } else if (rows[i].k === "-") { deleted++; }
2274
+ }
2275
+ return { added: added, deleted: deleted };
2276
+ }
2277
+ /* 同一文件在回合内被改多次时按 old→new 链折成一份净变化;链断返回 null(分段显示)。 */
2278
+ function foldParts(parts) {
2279
+ if (!parts || parts.length === 0) { return null; }
2280
+ var before = parts[0][0] == null ? "" : String(parts[0][0]);
2281
+ var after = parts[0][1] == null ? "" : String(parts[0][1]);
2282
+ for (var i = 1; i < parts.length; i++) {
2283
+ var o = parts[i][0];
2284
+ if (o != null && String(o) !== after) { return null; }
2285
+ if (parts[i][1] != null) { after = String(parts[i][1]); }
2286
+ }
2287
+ return { before: before, after: after };
2288
+ }
2289
+ function countParts(parts) {
2290
+ var added = 0, deleted = 0;
2291
+ for (var i = 0; i < parts.length; i++) {
2292
+ var c = countRows(diffRows(parts[i][0], parts[i][1]));
2293
+ added += c.added; deleted += c.deleted;
2294
+ }
2295
+ return { added: added, deleted: deleted };
2296
+ }
2297
+ function metaDiffs(data) {
2298
+ if (!data || typeof data !== "object") { return null; }
2299
+ var meta = data.meta;
2300
+ if (!meta || typeof meta !== "object") { return null; }
2301
+ var list = meta.diffs;
2302
+ if (!Array.isArray(list) || list.length === 0) { return null; }
2303
+ var out = [];
2304
+ for (var i = 0; i < list.length; i++) {
2305
+ var d = list[i];
2306
+ if (d && typeof d.path === "string" && d.path) { out.push(d); }
2307
+ }
2308
+ return out.length > 0 ? out : null;
2309
+ }
2310
+ function mergeDiff(files, diff) {
2311
+ var path = String(diff.path);
2312
+ var part = [diff.oldText == null ? null : String(diff.oldText), diff.newText == null ? "" : String(diff.newText)];
2313
+ for (var i = 0; i < files.length; i++) {
2314
+ if (files[i].path === path) {
2315
+ var copy = files.slice();
2316
+ copy[i] = { path: path, parts: files[i].parts.concat([part]) };
2317
+ return copy;
2318
+ }
2319
+ }
2320
+ return files.concat([{ path: path, parts: [part] }]);
2321
+ }
2322
+ function changesDefinition() {
2323
+ return {
2324
+ kind: CHANGES_KIND,
2325
+ match: function (event) {
2326
+ var d = (event && event.data) || {};
2327
+ if (event.type === "turn/start") {
2328
+ return d.turn === undefined ? null : { id: String(d.turn), role: "start" };
2329
+ }
2330
+ if (event.type === "tool/call") {
2331
+ if (d.callId !== undefined && d.turn !== undefined) { rootTurn[String(d.callId)] = d.turn; }
2332
+ return d.turn === undefined ? null : { id: String(d.turn), role: "update" };
2333
+ }
2334
+ if (event.type === "workspace/changes") {
2335
+ return d.turn === undefined ? null : { id: String(d.turn), role: "update" };
2336
+ }
2337
+ if (event.type === "tool/result") {
2338
+ return (d.turn !== undefined && metaDiffs(d)) ? { id: String(d.turn), role: "update" } : null;
2339
+ }
2340
+ if (event.type === "tool/ptc-dispatch") {
2341
+ if (!metaDiffs(d)) { return null; }
2342
+ var turn = rootTurn[String(d.rootCallId)];
2343
+ return turn === undefined ? null : { id: String(turn), role: "update" };
2344
+ }
2345
+ return null;
2346
+ },
2347
+ start: function () { return { seq: null, files: [] }; },
2348
+ update: function (context, match) {
2349
+ var prev = (context && context.state) || context || { seq: null, files: [] };
2350
+ var next = { seq: prev.seq == null ? null : prev.seq, files: prev.files || [] };
2351
+ var event = match && match.event;
2352
+ if (!event) { return next; }
2353
+ if (event.type === "workspace/changes") { return { seq: event.seq, files: next.files }; }
2354
+ var list = metaDiffs(event.data || {});
2355
+ if (!list) { return next; }
2356
+ var files = next.files;
2357
+ for (var i = 0; i < list.length; i++) { files = mergeDiff(files, list[i]); }
2358
+ return { seq: next.seq, files: files };
2359
+ }
2360
+ };
2361
+ }
2362
+ function shortPath(p) {
2363
+ var s = String(p);
2364
+ return s.length > 64 ? "…" + s.slice(s.length - 63) : s;
2365
+ }
2366
+ function changesCard(props) {
2367
+ var owner = props || {};
2368
+ var state = null;
2369
+ try {
2370
+ if (owner.turn && owner.turn.data && typeof owner.turn.data.get === "function") {
2371
+ state = owner.turn.data.get(CHANGES_KIND);
2372
+ }
2373
+ } catch (e) { state = null; }
2374
+ var sessionId = owner.sessionId;
2375
+ var seq = state && state.seq != null ? state.seq : null;
2376
+ var key = (sessionId && seq != null) ? String(sessionId) + ":" + String(seq) : null;
2377
+ var cached = key && probeCache[key] ? probeCache[key] : null;
2378
+ var probeState = REACT.useState(cached || "pending");
2379
+ var probe = probeState[0], setProbe = probeState[1];
2380
+ var expState = REACT.useState(false);
2381
+ var expanded = expState[0], setExpanded = expState[1];
2382
+ REACT.useEffect(function () {
2383
+ if (!key || cached) { return undefined; }
2384
+ var cancelled = false;
2385
+ try {
2386
+ fetch(SUMMARY_ROUTE + "?" + new URLSearchParams({ sessionId: String(sessionId), seq: String(seq) }),
2387
+ { cache: "no-store", credentials: "same-origin" }).then(function (res) {
2388
+ if (cancelled) { return; }
2389
+ var v = res.status === 200 ? "ok" : (res.status === 404 ? "missing" : "error");
2390
+ if (v !== "error" && probeCache[key] === undefined) { probeCache[key] = v; }
2391
+ setProbe(v);
2392
+ }).catch(function () {});
2393
+ } catch (e) {}
2394
+ return function () { cancelled = true; };
2395
+ }, [key, cached]);
2396
+ /* 只在「官方已失效」时才补位:ok / pending / error 一律不画,避免出现双份卡。 */
2397
+ if (!state || !state.files || state.files.length === 0 || seq == null || probe !== "missing") { return null; }
2398
+
2399
+ var files = state.files, totals = { added: 0, deleted: 0 }, prepared = [], i, j;
2400
+ for (i = 0; i < files.length; i++) {
2401
+ var net = foldParts(files[i].parts);
2402
+ var rows = (expanded && net !== null) ? diffRows(net.before, net.after) : null;
2403
+ var counted = net !== null ? countRows(diffRows(net.before, net.after)) : countParts(files[i].parts);
2404
+ totals.added += counted.added; totals.deleted += counted.deleted;
2405
+ prepared.push({ path: files[i].path, parts: files[i].parts, added: counted.added, deleted: counted.deleted, rows: rows });
2406
+ }
2407
+
2408
+ var mono = "ui-monospace, SFMono-Regular, Menlo, Consolas, monospace";
2409
+ var dim = { opacity: 0.6, fontSize: 11 };
2410
+ var add = { color: "#3fb950" };
2411
+ var del = { color: "#f85149" };
2412
+ var children = [];
2413
+ children.push(REACT.createElement("div", {
2414
+ key: "head",
2415
+ style: { display: "flex", alignItems: "center", gap: 8, cursor: "pointer", userSelect: "none" },
2416
+ onClick: function () { setExpanded(!expanded); }
2417
+ },
2418
+ REACT.createElement("span", { style: { fontWeight: 600 } }, "本回合改动"),
2419
+ REACT.createElement("span", { style: dim }, files.length + " 个文件"),
2420
+ REACT.createElement("span", { style: add }, "+" + totals.added),
2421
+ REACT.createElement("span", { style: del }, "−" + totals.deleted),
2422
+ REACT.createElement("span", { style: { marginLeft: "auto", opacity: 0.6, fontSize: 11 } }, expanded ? "收起" : "展开")
2423
+ ));
2424
+ children.push(REACT.createElement("div", { key: "note", style: { opacity: 0.5, fontSize: 11, marginTop: 2 } },
2425
+ "(据会话日志重建 · 仅文件工具的改动)"));
2426
+ if (expanded) {
2427
+ for (i = 0; i < prepared.length; i++) {
2428
+ var f = prepared[i];
2429
+ var block = [];
2430
+ block.push(REACT.createElement("div", {
2431
+ key: "p", style: { display: "flex", alignItems: "center", gap: 6, marginTop: 6 }
2432
+ },
2433
+ REACT.createElement("span", { style: { overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", fontFamily: mono }, title: f.path }, shortPath(f.path)),
2434
+ REACT.createElement("span", { style: add }, "+" + f.added),
2435
+ REACT.createElement("span", { style: del }, "−" + f.deleted)
2436
+ ));
2437
+ if (f.rows) {
2438
+ var shown = f.rows.length > DIFF_ROW_LIMIT ? f.rows.slice(0, DIFF_ROW_LIMIT) : f.rows;
2439
+ for (j = 0; j < shown.length; j++) {
2440
+ block.push(REACT.createElement("div", {
2441
+ key: "r" + j,
2442
+ style: { fontFamily: mono, fontSize: 11, whiteSpace: "pre-wrap", wordBreak: "break-all",
2443
+ color: shown[j].k === "+" ? "#3fb950" : (shown[j].k === "-" ? "#f85149" : "inherit"),
2444
+ opacity: shown[j].k === " " ? 0.7 : 1 }
2445
+ }, (shown[j].k === " " ? " " : shown[j].k + " ") + shown[j].t));
2446
+ }
2447
+ if (f.rows.length > DIFF_ROW_LIMIT) {
2448
+ block.push(REACT.createElement("div", { key: "more", style: dim }, "…还有 " + (f.rows.length - DIFF_ROW_LIMIT) + " 行"));
2449
+ }
2450
+ } else {
2451
+ for (j = 0; j < f.parts.length; j++) {
2452
+ block.push(REACT.createElement("div", { key: "seg" + j, style: { opacity: 0.6, fontSize: 11, marginTop: 4 } },
2453
+ "第 " + (j + 1) + " 段改动(与前一段不衔接,分段显示)"));
2454
+ var prows = diffRows(f.parts[j][0], f.parts[j][1]);
2455
+ var pshown = prows.length > DIFF_ROW_LIMIT ? prows.slice(0, DIFF_ROW_LIMIT) : prows;
2456
+ for (var r2 = 0; r2 < pshown.length; r2++) {
2457
+ block.push(REACT.createElement("div", {
2458
+ key: "pr" + j + "_" + r2,
2459
+ style: { fontFamily: mono, fontSize: 11, whiteSpace: "pre-wrap", wordBreak: "break-all",
2460
+ color: pshown[r2].k === "+" ? "#3fb950" : (pshown[r2].k === "-" ? "#f85149" : "inherit"),
2461
+ opacity: pshown[r2].k === " " ? 0.7 : 1 }
2462
+ }, (pshown[r2].k === " " ? " " : pshown[r2].k + " ") + pshown[r2].t));
2463
+ }
2464
+ }
2465
+ }
2466
+ children.push(REACT.createElement("div", { key: "f" + i }, block));
2467
+ }
2468
+ }
2469
+ return REACT.createElement("div", {
2470
+ style: { marginTop: 8, border: "1px solid var(--dsw-alias-border-neutral-secondary, rgba(128,128,128,0.25))",
2471
+ borderRadius: 8, padding: "8px 10px", fontSize: 12 }
2472
+ }, children);
2473
+ }
2474
+ function installChangesCard(scope, ctx) {
2475
+ if (!REACT) { throw new Error("ui-tweaks: require(\"react\") 不可用,跳过改动补位卡"); }
2476
+ var s = scope || ctx || {};
2477
+ var conv = s.uiConversation || (ctx && ctx.uiConversation) || null;
2478
+ var slots = s.slots || (ctx && ctx.slots) || null;
2479
+ if (!conv || !conv.events || typeof conv.events.register !== "function") {
2480
+ throw new Error("ui-tweaks: uiConversation.events 不可用");
2481
+ }
2482
+ if (!slots || typeof slots.register !== "function" || typeof slots.inject !== "function") {
2483
+ throw new Error("ui-tweaks: slots 服务不可用");
2484
+ }
2485
+ conv.events.register(changesDefinition());
2486
+ slots.inject("conversation.chat.turnTail", function () {
2487
+ return slots.register({
2488
+ name: "conversation.chat.turnTail",
2489
+ id: CHANGES_KIND,
2490
+ order: 30,
2491
+ inject: function (sessionId) { return { sessionId: sessionId }; }
2492
+ }, changesCard);
2493
+ });
2494
+ try { ctx.on("connection/reset", function () { probeCache = {}; rootTurn = {}; }); } catch (e) {}
2495
+ }
2496
+
2080
2497
  function apply(ctx) {
2081
2498
  try { window.__DSHUIW_BUILD__ = BUILD; } catch (e) {}
2082
2499
  guard("tablefix", installTableWidthFix);
2083
2500
  guard("scrollbar", installLeftScrollbar);
2084
2501
  guard("updates", function () { installPluginUpdates(ctx); });
2085
2502
  guard("anim", installAnimationFix);
2503
+ guard("changes", function () { ctx.inject(["uiConversation", "slots"], function (s) { installChangesCard(s, ctx); }); });
2086
2504
  }
2087
2505
 
2088
2506
  module.exports = { name: "ui-tweaks", apply: apply, inject: ["remote", "remote.pluginManager", "remote.pluginInventory"] };