dsh-layered-memory 0.8.2 → 0.8.3

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/dist/client.js CHANGED
@@ -770,15 +770,56 @@ window.__ModuleLoader__.load({
770
770
  ".dsh-mem-btn:focus-visible { outline: 2px solid var(--dsh-mem-accent); outline-offset: 1px; }",
771
771
  ".dsh-mem-btn:disabled { opacity: 0.45; cursor: not-allowed; }",
772
772
  // ── 控件:输入框 / 下拉 ──
773
- ".dsh-mem-input, .dsh-mem-select {",
773
+ ".dsh-mem-input {",
774
774
  " padding: 5px 10px; font-size: 13px; border-radius: 8px; color: var(--dsh-mem-text-1);",
775
775
  " border: 1px solid var(--dsh-mem-border); background: var(--dsh-mem-bg-card);",
776
776
  " transition: border-color .15s ease, box-shadow .15s ease;",
777
777
  "}",
778
- ".dsh-mem-input:focus, .dsh-mem-select:focus {",
778
+ ".dsh-mem-input:focus {",
779
779
  " outline: none; border-color: var(--dsh-mem-accent);",
780
780
  " box-shadow: 0 0 0 3px var(--dsh-mem-accent-weak);",
781
781
  "}",
782
+ // ── 下拉(NSel 触发钮):观感同输入框(8px 圆角/同令牌边框底色),文字
783
+ // ellipsis + CSS 描边 chevron(展开旋转 180°);弹出面板见 .dsh-mem-pop ──
784
+ ".dsh-mem-select {",
785
+ " display: inline-flex; align-items: center; justify-content: space-between; gap: 8px;",
786
+ " width: 100%; min-width: 0; padding: 5px 10px; font: inherit; font-size: 13px; line-height: 20px;",
787
+ " text-align: left; border-radius: 8px; cursor: pointer; color: var(--dsh-mem-text-1);",
788
+ " border: 1px solid var(--dsh-mem-border); background: var(--dsh-mem-bg-card);",
789
+ " transition: border-color .15s ease, box-shadow .15s ease;",
790
+ "}",
791
+ ".dsh-mem-select:focus-visible {",
792
+ " outline: none; border-color: var(--dsh-mem-accent);",
793
+ " box-shadow: 0 0 0 3px var(--dsh-mem-accent-weak);",
794
+ "}",
795
+ ".dsh-mem-select:disabled { opacity: 0.45; cursor: not-allowed; }",
796
+ ".dsh-mem-select-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }",
797
+ // 下拉弹出面板:dsh MenuDropdown 同款——浮层 12 圆角 + menu 底 + lv3 投影,
798
+ // 选项行 10 圆角 + hover 底色 + 选中打勾(选项 active 由 data-active 标记)
799
+ ".dsh-mem-sel { position: relative; display: inline-flex; min-width: 0; vertical-align: top; }",
800
+ ".dsh-mem-sel-chev {",
801
+ " width: 8px; height: 8px; flex: none; margin-right: 2px;",
802
+ " border-right: 1.5px solid var(--dsh-mem-text-3); border-bottom: 1.5px solid var(--dsh-mem-text-3);",
803
+ " transform: rotate(45deg); transition: transform .12s ease;",
804
+ "}",
805
+ ".dsh-mem-sel-chev-open { transform: rotate(225deg); }",
806
+ ".dsh-mem-pop {",
807
+ " position: absolute; top: calc(100% + 6px); left: 0; z-index: 30;",
808
+ " min-width: 100%; width: max-content; max-width: 340px; max-height: 264px;",
809
+ " overflow-y: auto; overscroll-behavior: contain; padding: 4px;",
810
+ " background: var(--dsh-mem-bg-pop); border: 1px solid var(--dsh-mem-border-pop);",
811
+ " border-radius: 12px; box-shadow: var(--dsh-mem-shadow-pop);",
812
+ "}",
813
+ ".dsh-mem-pop-opt {",
814
+ " display: flex; align-items: center; gap: 8px; width: 100%; box-sizing: border-box;",
815
+ " min-height: 32px; padding: 5px 10px; font: inherit; font-size: 13px; line-height: 20px;",
816
+ " text-align: left; color: var(--dsh-mem-text-1); cursor: pointer;",
817
+ " background: none; border: none; outline: none; border-radius: 10px;",
818
+ "}",
819
+ ".dsh-mem-pop-opt:hover, .dsh-mem-pop-opt[data-active=\"1\"] { background: var(--dsh-mem-bg-hover); }",
820
+ ".dsh-mem-pop-opt-label { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }",
821
+ ".dsh-mem-pop-check { flex: none; font-size: 12px; color: var(--dsh-mem-text-1); }",
822
+ ".dsh-mem-pop-empty { padding: 10px; font-size: 13px; color: var(--dsh-mem-text-3); }",
782
823
  // ── Tab(下划线式):active 品牌蓝下划线 + 主文字色 ──
783
824
  ".dsh-mem-tab {",
784
825
  " padding: 6px 12px; font-size: 13px; cursor: pointer; background: none; border: none;",
@@ -793,6 +834,8 @@ window.__ModuleLoader__.load({
793
834
  " box-shadow: var(--dsh-mem-shadow-card);",
794
835
  "}",
795
836
  ".dsh-mem-card-hover:hover { border-color: var(--dsh-mem-border-strong); }",
837
+ // ── 场景卡折叠箭头:展开态旋转 90°(过渡只做反馈,进 reduced-motion 压制名单) ──
838
+ ".dsh-mem-scene-chev { display: inline-block; transition: transform .15s ease; color: var(--dsh-mem-text-3); }",
796
839
  // ── 记忆类型标签:tint 风格(彩底淡色 + 彩字),--dsh-mem-tag-c 由类型类给定 ──
797
840
  ".dsh-mem-tag {",
798
841
  " display: inline-block; padding: 1px 8px; border-radius: 999px;",
@@ -889,7 +932,7 @@ window.__ModuleLoader__.load({
889
932
  ".dsh-mem-rb-muted { font-size: 12px; color: var(--dsh-mem-text-3); }",
890
933
  // reduced-motion 兜底放样式表末尾:同特异性下后置声明才能压过上面的组件类
891
934
  "@media (prefers-reduced-motion: reduce) {",
892
- " .dsh-mem-root, .dsh-mem-root *, .dsh-mem-btn, .dsh-mem-input, .dsh-mem-select, .dsh-mem-rb-fill { transition: none; }",
935
+ " .dsh-mem-root, .dsh-mem-root *, .dsh-mem-btn, .dsh-mem-input, .dsh-mem-select, .dsh-mem-rb-fill, .dsh-mem-scene-chev, .dsh-mem-sel-chev { transition: none; }",
893
936
  " .dsh-mem-flow { animation: none; }",
894
937
  "}",
895
938
  ].join("\n");
@@ -1463,6 +1506,158 @@ window.__ModuleLoader__.load({
1463
1506
  );
1464
1507
  }
1465
1508
 
1509
+ // ── 自绘下拉(NSel):对齐 dsh MenuDropdown(输入栏模型选择器同款)观感。
1510
+ // 原生 <select> 的弹出列表是操作系统绘的(方角、系统高亮色),appearance:none
1511
+ // 只能改闭合态外壳——所以整件换成按钮触发 + 浮层面板:面板 12px 圆角 /
1512
+ // bg-pop / border-pop / shadow-pop,选项行 10px 圆角 + hover 底色 + 选中打勾。
1513
+ // 键盘 ↑↓ 移动(wrap)、回车/空格开面板与选定、Esc/外点/焦点离开收起;
1514
+ // 焦点始终留在触发钮上(keydown 从钮冒泡到包装层统一处理)。 ──
1515
+ function NSel(props) {
1516
+ var options = props.options || [];
1517
+ var value = props.value || "";
1518
+ var disabled = !!props.disabled;
1519
+ var openState = react.useState(false);
1520
+ var open = openState[0];
1521
+ var setOpen = openState[1];
1522
+ var idxState = react.useState(-1);
1523
+ var idx = idxState[0];
1524
+ var setIdx = idxState[1];
1525
+ var wrapRef = react.useRef(null);
1526
+ var listRef = react.useRef(null);
1527
+
1528
+ var selectedLabel = "";
1529
+ for (var si = 0; si < options.length; si++) {
1530
+ if (options[si].id === value) selectedLabel = options[si].label;
1531
+ }
1532
+
1533
+ react.useEffect(function () {
1534
+ if (!open) return undefined;
1535
+ var onDown = function (e) {
1536
+ if (wrapRef.current && !wrapRef.current.contains(e.target)) setOpen(false);
1537
+ };
1538
+ document.addEventListener("mousedown", onDown);
1539
+ return function () { document.removeEventListener("mousedown", onDown); };
1540
+ }, [open]);
1541
+
1542
+ // 键盘移动后把活动项滚进可视区(鼠标 hover 同步活动项索引)
1543
+ react.useEffect(function () {
1544
+ if (!open || !listRef.current) return;
1545
+ var el = listRef.current.querySelector('[data-active="1"]');
1546
+ if (el && el.scrollIntoView) el.scrollIntoView({ block: "nearest" });
1547
+ }, [open, idx]);
1548
+
1549
+ var indexOfValue = function () {
1550
+ for (var i = 0; i < options.length; i++) if (options[i].id === value) return i;
1551
+ return -1;
1552
+ };
1553
+ var closeMenu = function (refocus) {
1554
+ setOpen(false);
1555
+ setIdx(-1);
1556
+ if (refocus && wrapRef.current) {
1557
+ var btn = wrapRef.current.querySelector("button");
1558
+ if (btn && btn.focus) btn.focus();
1559
+ }
1560
+ };
1561
+ var pick = function (id) {
1562
+ closeMenu(true);
1563
+ if (id !== value && props.onChange) props.onChange(id);
1564
+ };
1565
+ var moveActive = function (delta) {
1566
+ var n = options.length;
1567
+ if (n === 0) return;
1568
+ var cur = idx >= 0 ? idx : indexOfValue();
1569
+ if (cur < 0) cur = delta > 0 ? -1 : 0;
1570
+ setIdx(delta > 0 ? (cur + 1) % n : (cur - 1 + n) % n);
1571
+ };
1572
+ var onKey = function (e) {
1573
+ if (disabled) return;
1574
+ if (e.key === "Escape") {
1575
+ if (open) { e.preventDefault(); closeMenu(true); }
1576
+ return;
1577
+ }
1578
+ if (e.key === "ArrowDown" || e.key === "ArrowUp") {
1579
+ e.preventDefault();
1580
+ if (!open) { setOpen(true); setIdx(indexOfValue()); }
1581
+ else moveActive(e.key === "ArrowDown" ? 1 : -1);
1582
+ return;
1583
+ }
1584
+ if (!open && (e.key === "Enter" || e.key === " ")) {
1585
+ e.preventDefault();
1586
+ setOpen(true);
1587
+ setIdx(indexOfValue());
1588
+ return;
1589
+ }
1590
+ if (open && e.key === "Enter") {
1591
+ e.preventDefault();
1592
+ var t = idx >= 0 ? idx : indexOfValue();
1593
+ if (t < 0) t = 0;
1594
+ if (options[t]) pick(options[t].id);
1595
+ }
1596
+ };
1597
+ var onBlur = function (e) {
1598
+ if (!open) return;
1599
+ var to = e.relatedTarget;
1600
+ if (!to || (wrapRef.current && !wrapRef.current.contains(to))) setOpen(false);
1601
+ };
1602
+
1603
+ return react.createElement(
1604
+ "div",
1605
+ { className: "dsh-mem-sel", style: props.style, ref: wrapRef, onKeyDown: onKey, onBlur: onBlur },
1606
+ react.createElement(
1607
+ "button",
1608
+ {
1609
+ type: "button",
1610
+ className: "dsh-mem-select" + (open ? " dsh-mem-select-open" : ""),
1611
+ disabled: disabled,
1612
+ "aria-haspopup": "listbox",
1613
+ "aria-expanded": open,
1614
+ onClick: function () {
1615
+ if (open) closeMenu(false);
1616
+ else { setOpen(true); setIdx(-1); }
1617
+ },
1618
+ },
1619
+ react.createElement("span", { className: "dsh-mem-select-label" }, selectedLabel || props.placeholder || "(请选择)"),
1620
+ react.createElement("span", {
1621
+ className: "dsh-mem-sel-chev" + (open ? " dsh-mem-sel-chev-open" : ""),
1622
+ "aria-hidden": true,
1623
+ }),
1624
+ ),
1625
+ open && !disabled
1626
+ ? react.createElement(
1627
+ "div", { className: "dsh-mem-pop", ref: listRef, role: "listbox" },
1628
+ options.length === 0
1629
+ ? react.createElement("div", { className: "dsh-mem-pop-empty" }, "无选项")
1630
+ : options.map(function (o, i) {
1631
+ return react.createElement(
1632
+ "button",
1633
+ {
1634
+ type: "button",
1635
+ key: o.id,
1636
+ className: "dsh-mem-pop-opt" + (o.id === value ? " dsh-mem-pop-opt-on" : ""),
1637
+ role: "option",
1638
+ "aria-selected": o.id === value,
1639
+ "data-active": i === idx ? "1" : "0",
1640
+ // 阻断 mousedown 默认行为(抢焦点):焦点留在触发钮上,避免
1641
+ // blur-关面板把后续 click 吞掉(点选项无反应的事故根因)
1642
+ onMouseDown: function (e) { if (e.preventDefault) e.preventDefault(); },
1643
+ onClick: function () { pick(o.id); },
1644
+ onMouseEnter: function () { if (idx !== i) setIdx(i); },
1645
+ },
1646
+ react.createElement("span", { className: "dsh-mem-pop-opt-label" }, o.label),
1647
+ o.id === value
1648
+ ? react.createElement("span", { className: "dsh-mem-pop-check" }, "✓")
1649
+ : null,
1650
+ );
1651
+ }),
1652
+ )
1653
+ : null,
1654
+ );
1655
+ }
1656
+
1657
+ // 供应商 → 模型列表缓存(模块级,会话内存活):切换供应商时缓存命中即时渲染
1658
+ // (后台仍刷新),面板首次加载时预取全部供应商——消除"切换后模型按钮几秒真空期"
1659
+ var modelsCache = {};
1660
+
1466
1661
  // ── 蒸馏模型选择器(供应商/模型两级下拉)──
1467
1662
  // 数据源:dsh-memory/llm-providers(供应商目录 + 当前覆盖 + 实际生效路由)与
1468
1663
  // dsh-memory/llm-models(所选供应商的模型列表)。写入走 settings-set 的
@@ -1483,14 +1678,21 @@ window.__ModuleLoader__.load({
1483
1678
  var manualModel = manualState[0];
1484
1679
  var setManualModel = manualState[1];
1485
1680
 
1681
+ // 写入在途时丢弃轮询响应(在途请求读到的是写入前的旧值,直接 set 会把
1682
+ // 乐观更新闪回旧文本);写入成功后主动拉一次服务器真值收敛
1683
+ var pendingWrites = react.useRef(0);
1684
+
1685
+ function refreshInfo() {
1686
+ rpc("dsh-memory/llm-providers", {})
1687
+ .then(function (r) {
1688
+ if (r && r.ok && pendingWrites.current === 0) setInfo(r.value);
1689
+ })
1690
+ .catch(function () {});
1691
+ }
1692
+
1486
1693
  react.useEffect(function () {
1487
- var load = function () {
1488
- rpc("dsh-memory/llm-providers", {})
1489
- .then(function (r) { if (r && r.ok) setInfo(r.value); })
1490
- .catch(function () {});
1491
- };
1492
- load();
1493
- var timer = setInterval(load, 5000);
1694
+ refreshInfo();
1695
+ var timer = setInterval(refreshInfo, 5000);
1494
1696
  return function () { clearInterval(timer); };
1495
1697
  }, [rpc]);
1496
1698
 
@@ -1500,32 +1702,80 @@ window.__ModuleLoader__.load({
1500
1702
  var curProvider = draft || (info && info.current ? info.current.provider : "");
1501
1703
  var curModel = info && info.current ? info.current.model : "";
1502
1704
 
1705
+ // 预取各供应商模型列表进缓存(后台、失败忽略):llm-providers 轮询每次都会
1706
+ // 走一遍,缓存齐了以后是空循环
1707
+ react.useEffect(function () {
1708
+ if (!info || !info.providers) return;
1709
+ info.providers.forEach(function (p) {
1710
+ if (!p.id || modelsCache[p.id]) return;
1711
+ rpc("dsh-memory/llm-models", { provider: p.id })
1712
+ .then(function (r) { if (r && r.ok && r.value) modelsCache[p.id] = r.value.models || []; })
1713
+ .catch(function () {});
1714
+ });
1715
+ }, [rpc, info]);
1716
+
1503
1717
  react.useEffect(function () {
1504
1718
  if (!curProvider) { setModels(null); return undefined; }
1505
1719
  var alive = true;
1506
- setModels(null);
1720
+ var cached = modelsCache[curProvider];
1721
+ var picked = false;
1722
+ // 缓存命中:同步先渲染(跳过"加载中"真空期),后台仍刷新一遍
1723
+ if (cached) {
1724
+ setModels(cached);
1725
+ // 供应商切换自动落第一个模型(缓存路径):当前覆盖不属于该供应商 → 成对写覆盖
1726
+ if (draft && cached.length > 0 && !cached.some(function (m) { return m.id === curModel; })) {
1727
+ picked = true;
1728
+ writeLlm({ distillProvider: draft, distillModel: cached[0].id });
1729
+ }
1730
+ } else {
1731
+ setModels(null);
1732
+ }
1507
1733
  rpc("dsh-memory/llm-models", { provider: curProvider })
1508
- .then(function (r) { if (alive && r && r.ok) setModels(r.value.models || []); })
1509
- .catch(function () { if (alive) setModels([]); });
1734
+ .then(function (r) {
1735
+ if (!alive) return;
1736
+ var list = (r && r.ok && r.value && r.value.models) || [];
1737
+ modelsCache[curProvider] = list;
1738
+ setModels(list);
1739
+ // 自动落第一个模型(网络路径,缓存未命中或缓存路径未写时)
1740
+ if (!picked && draft && list.length > 0 && !list.some(function (m) { return m.id === curModel; })) {
1741
+ writeLlm({ distillProvider: draft, distillModel: list[0].id });
1742
+ }
1743
+ })
1744
+ .catch(function () { if (alive && !cached) setModels([]); });
1510
1745
  return function () { alive = false; };
1511
1746
  }, [rpc, curProvider]);
1512
1747
 
1513
1748
  var writeLlm = function (patch) {
1514
1749
  if (!info) return;
1515
- // 乐观更新:立即写视图字段,失败回滚
1750
+ // 乐观更新:覆盖必须写成视图键 current.provider/model(按钮文本读这对键)——
1751
+ // 直接合并 settings 键 distillProvider/distillModel 对显示层是空操作,文本要
1752
+ // 等 5s 轮询才变;effective 同步推导("当前生效"描述行),失败回滚
1516
1753
  var prev = info;
1754
+ var prevCurrent = prev.current || {};
1755
+ var np = patch.distillProvider !== undefined ? patch.distillProvider : prevCurrent.provider;
1756
+ var nm = patch.distillModel !== undefined ? patch.distillModel : prevCurrent.model;
1757
+ var nextEffective;
1758
+ if (np && nm) nextEffective = { provider: np, model: nm };
1759
+ else if (prev.default) nextEffective = { provider: prev.default.provider, model: prev.default.model };
1760
+ else nextEffective = null;
1517
1761
  setInfo(Object.assign({}, prev, {
1518
- current: Object.assign({}, prev.current, patch),
1762
+ current: Object.assign({}, prevCurrent, { provider: np, model: nm }),
1763
+ effective: nextEffective,
1519
1764
  }));
1520
1765
  setDraft("");
1766
+ pendingWrites.current += 1;
1521
1767
  rpc("dsh-memory/settings-set", patch)
1522
1768
  .then(function (r) {
1769
+ pendingWrites.current -= 1;
1523
1770
  if (!r || !r.ok) {
1524
1771
  setInfo(prev);
1525
1772
  setDraft(draft);
1773
+ } else {
1774
+ refreshInfo();
1526
1775
  }
1527
1776
  })
1528
1777
  .catch(function () {
1778
+ pendingWrites.current -= 1;
1529
1779
  setInfo(prev);
1530
1780
  setDraft(draft);
1531
1781
  });
@@ -1540,7 +1790,8 @@ window.__ModuleLoader__.load({
1540
1790
  react.createElement("div", { style: S.switchLabel }, "蒸馏模型"),
1541
1791
  react.createElement(
1542
1792
  "div", { style: S.switchDesc },
1543
- "已由部署配置固定:" + info.effective.provider + " / " + info.effective.model,
1793
+ "已由部署配置固定:" + info.effective.provider + " / " + info.effective.model +
1794
+ "(如需在页面切换,请移除 profile cordis.patch.yml 中 llm 的 provider/model 覆盖)",
1544
1795
  )),
1545
1796
  );
1546
1797
  }
@@ -1563,35 +1814,29 @@ window.__ModuleLoader__.load({
1563
1814
  providerOptions.push({ id: curProvider, label: curProvider + "(已不在列表)" });
1564
1815
  }
1565
1816
 
1566
- var modelOptions = [{ id: "", label: "跟随默认" }].concat(
1567
- (models || []).map(function (m) { return { id: m.id, label: m.name !== m.id ? m.name + "(" + m.id + ")" : m.id }; }),
1568
- );
1817
+ var modelOptions = (models || []).map(function (m) {
1818
+ return { id: m.id, label: m.name !== m.id ? m.name + "(" + m.id + ")" : m.id };
1819
+ });
1569
1820
  if (curModel && !modelKnown) {
1570
1821
  modelOptions.push({ id: curModel, label: curModel + "(已不在列表)" });
1571
1822
  }
1572
1823
 
1573
1824
  return react.createElement(
1574
1825
  "div", { style: Object.assign({}, S.switchRow, { flexWrap: "wrap" }) },
1575
- react.createElement(
1576
- "select",
1577
- {
1578
- className: "dsh-mem-select",
1579
- style: { flexShrink: 0, maxWidth: 220 },
1580
- value: curProvider,
1581
- disabled: disabled,
1582
- onChange: function (e) {
1583
- var v = e.target.value;
1584
- if (v === "") {
1585
- writeLlm({ distillProvider: "", distillModel: "" });
1586
- } else {
1587
- setDraft(v);
1588
- }
1589
- },
1826
+ react.createElement(NSel, {
1827
+ style: { flexShrink: 0, width: 220 },
1828
+ options: providerOptions,
1829
+ value: curProvider,
1830
+ disabled: disabled,
1831
+ placeholder: "跟随默认",
1832
+ onChange: function (v) {
1833
+ if (v === "") {
1834
+ writeLlm({ distillProvider: "", distillModel: "" });
1835
+ } else {
1836
+ setDraft(v);
1837
+ }
1590
1838
  },
1591
- providerOptions.map(function (o) {
1592
- return react.createElement("option", { key: o.id, value: o.id }, o.label);
1593
- }),
1594
- ),
1839
+ }),
1595
1840
  !curProvider
1596
1841
  ? null
1597
1842
  : manualInput
@@ -1607,21 +1852,17 @@ window.__ModuleLoader__.load({
1607
1852
  }
1608
1853
  },
1609
1854
  })
1610
- : react.createElement(
1611
- "select",
1612
- {
1613
- className: "dsh-mem-select",
1614
- style: { flex: 1, minWidth: 160 },
1615
- value: curModel,
1616
- disabled: disabled || models === null,
1617
- onChange: function (e) {
1618
- writeLlm({ distillProvider: curProvider, distillModel: e.target.value });
1619
- },
1855
+ : react.createElement(NSel, {
1856
+ style: { flex: 1, minWidth: 160 },
1857
+ options: modelOptions,
1858
+ // 拉取期(无缓存首切)显示占位而非上一供应商的过期模型名
1859
+ value: models === null ? "" : curModel,
1860
+ disabled: disabled || models === null,
1861
+ placeholder: models === null ? "加载模型列表…" : "(选择模型)",
1862
+ onChange: function (v) {
1863
+ writeLlm({ distillProvider: curProvider, distillModel: v });
1620
1864
  },
1621
- modelOptions.map(function (o) {
1622
- return react.createElement("option", { key: o.id, value: o.id }, o.label);
1623
- }),
1624
- ),
1865
+ }),
1625
1866
  react.createElement("div", null,
1626
1867
  react.createElement("div", { style: S.switchLabel }, "蒸馏模型"),
1627
1868
  react.createElement(
@@ -1866,7 +2107,7 @@ window.__ModuleLoader__.load({
1866
2107
  if (key === "reasoningEffort" && next.effort) {
1867
2108
  next.effort = Object.assign({}, prev.effort, {
1868
2109
  current: value,
1869
- effective: value || prev.effort.fallback,
2110
+ effective: value,
1870
2111
  });
1871
2112
  }
1872
2113
  setSettingsData(next);
@@ -1960,13 +2201,18 @@ window.__ModuleLoader__.load({
1960
2201
  "div",
1961
2202
  { style: S.switchRow },
1962
2203
  react.createElement(Segmented, {
1963
- value: settingsData.effort.current,
1964
- options: [
1965
- { key: "", label: "跟随配置" },
1966
- { key: "off", label: "off" },
1967
- { key: "high", label: "high" },
1968
- { key: "max", label: "max" },
1969
- ],
2204
+ // 档位表来自当前生效模型的能力声明(settings-get 的 effort.options;
2205
+ // 空声明 → 只显示 high)。''(自动)或档位不在表里时高亮实际生效值。
2206
+ value: (function () {
2207
+ var opts = settingsData.effort.options && settingsData.effort.options.length
2208
+ ? settingsData.effort.options : ["high"];
2209
+ if (opts.indexOf(settingsData.effort.current) >= 0) return settingsData.effort.current;
2210
+ return opts.indexOf(settingsData.effort.effective) >= 0
2211
+ ? settingsData.effort.effective : "high";
2212
+ })(),
2213
+ options: (settingsData.effort.options && settingsData.effort.options.length
2214
+ ? settingsData.effort.options : ["high"]
2215
+ ).map(function (k) { return { key: k, label: k }; }),
1970
2216
  disabled: !master,
1971
2217
  onChange: function (v) { toggle("reasoningEffort", v); },
1972
2218
  }),
@@ -1976,7 +2222,7 @@ window.__ModuleLoader__.load({
1976
2222
  "div",
1977
2223
  { style: S.switchDesc },
1978
2224
  "当前生效 " + (settingsData.effort.effective || "(不传,跟随模型默认)") +
1979
- (settingsData.effort.current ? "" : "(来自部署配置 llm.reasoningEffort)"),
2225
+ (settingsData.effort.current ? "" : "(自动)"),
1980
2226
  )),
1981
2227
  )
1982
2228
  : null,
@@ -2143,30 +2389,22 @@ window.__ModuleLoader__.load({
2143
2389
  onChange: function (e) { setQuery(e.target.value); },
2144
2390
  onKeyDown: function (e) { if (e.key === "Enter") search(); },
2145
2391
  }),
2146
- react.createElement(
2147
- "select",
2148
- {
2149
- className: "dsh-mem-select",
2150
- value: typeFilter,
2151
- onChange: function (e) { setTypeFilter(e.target.value); },
2152
- },
2153
- react.createElement("option", { value: "" }, "全部类型"),
2154
- typeChoices.map(function (t) {
2155
- return react.createElement("option", { key: t, value: t }, TYPE_LABELS[t] || t);
2156
- }),
2157
- ),
2158
- react.createElement(
2159
- "select",
2160
- {
2161
- className: "dsh-mem-select",
2162
- value: sceneFilter,
2163
- onChange: function (e) { setSceneFilter(e.target.value); },
2164
- },
2165
- react.createElement("option", { value: "" }, "全部情境"),
2166
- sceneOptions.map(function (s) {
2167
- return react.createElement("option", { key: s, value: s }, s.length > 24 ? s.slice(0, 24) + "…" : s);
2168
- }),
2169
- ),
2392
+ react.createElement(NSel, {
2393
+ style: { maxWidth: 200 },
2394
+ options: [{ id: "", label: "全部类型" }].concat(typeChoices.map(function (t) {
2395
+ return { id: t, label: TYPE_LABELS[t] || t };
2396
+ })),
2397
+ value: typeFilter,
2398
+ onChange: setTypeFilter,
2399
+ }),
2400
+ react.createElement(NSel, {
2401
+ style: { maxWidth: 220 },
2402
+ options: [{ id: "", label: "全部情境" }].concat(sceneOptions.map(function (s) {
2403
+ return { id: s, label: s.length > 24 ? s.slice(0, 24) + "…" : s };
2404
+ })),
2405
+ value: sceneFilter,
2406
+ onChange: setSceneFilter,
2407
+ }),
2170
2408
  react.createElement(NButton, { onClick: search }, "搜索"),
2171
2409
  ),
2172
2410
  react.createElement(
@@ -2243,6 +2481,44 @@ window.__ModuleLoader__.load({
2243
2481
  }
2244
2482
 
2245
2483
  // ── Tab:L2 场景 ──
2484
+ // ── 场景卡:默认收起(头部 + 摘要行),点击头部展开/收起正文——与记忆卡的
2485
+ // 点击展开范式同款(cursor pointer + card-hover 描边),箭头 ▸ 展开态旋转 90°。
2486
+ function SceneCard(props) {
2487
+ var s = props.s;
2488
+ var openState = react.useState(false);
2489
+ var open = openState[0];
2490
+ var setOpen = openState[1];
2491
+ return react.createElement(
2492
+ "div", { className: "dsh-mem-card dsh-mem-card-hover", style: S.card },
2493
+ react.createElement(
2494
+ "div",
2495
+ {
2496
+ style: Object.assign({}, S.sceneHead, { cursor: "pointer", userSelect: "none" }),
2497
+ onClick: function () { setOpen(!open); },
2498
+ },
2499
+ react.createElement(
2500
+ "span",
2501
+ { className: "dsh-mem-scene-chev", style: { transform: open ? "rotate(90deg)" : "none" } },
2502
+ "▸",
2503
+ ),
2504
+ react.createElement("span", { style: S.sceneTitle }, s.path),
2505
+ s.heat ? react.createElement("span", { style: S.muted }, "热度 " + s.heat) : null,
2506
+ react.createElement("div", { style: S.grow }),
2507
+ react.createElement("span", { style: S.muted }, "更新 " + fmtTime(s.updated)),
2508
+ ),
2509
+ s.summary
2510
+ ? react.createElement(
2511
+ "div",
2512
+ { style: Object.assign({}, S.muted, { marginBottom: 6 }) },
2513
+ s.summary,
2514
+ )
2515
+ : null,
2516
+ open
2517
+ ? react.createElement("pre", { style: S.pre }, s.content || "(空)")
2518
+ : null,
2519
+ );
2520
+ }
2521
+
2246
2522
  function ScenesTab(props) {
2247
2523
  var rpc = props.rpc;
2248
2524
  var itemsState = react.useState(null);
@@ -2276,24 +2552,7 @@ window.__ModuleLoader__.load({
2276
2552
  ? react.createElement("p", { style: S.intro }, "暂无场景块。累计 5 条新记忆后 L2 会自动整合出第一个场景。")
2277
2553
  : null,
2278
2554
  (items || []).map(function (s) {
2279
- return react.createElement(
2280
- "div", { key: s.path, className: "dsh-mem-card", style: S.card },
2281
- react.createElement(
2282
- "div", { style: S.sceneHead },
2283
- react.createElement("span", { style: S.sceneTitle }, s.path),
2284
- s.heat ? react.createElement("span", { style: S.muted }, "热度 " + s.heat) : null,
2285
- react.createElement("div", { style: S.grow }),
2286
- react.createElement("span", { style: S.muted }, "更新 " + fmtTime(s.updated)),
2287
- ),
2288
- s.summary
2289
- ? react.createElement(
2290
- "div",
2291
- { style: Object.assign({}, S.muted, { marginBottom: 6 }) },
2292
- s.summary,
2293
- )
2294
- : null,
2295
- react.createElement("pre", { style: S.pre }, s.content || "(空)"),
2296
- );
2555
+ return react.createElement(SceneCard, { key: s.path, s: s });
2297
2556
  }),
2298
2557
  );
2299
2558
  }
@@ -2353,6 +2612,7 @@ window.__ModuleLoader__.load({
2353
2612
  var errState = react.useState(null);
2354
2613
  var error = errState[0];
2355
2614
  var setError = errState[1];
2615
+ var preRef = react.useRef(null);
2356
2616
 
2357
2617
  var load = react.useCallback(function () {
2358
2618
  setError(null);
@@ -2365,6 +2625,10 @@ window.__ModuleLoader__.load({
2365
2625
  }, [rpc]);
2366
2626
 
2367
2627
  react.useEffect(function () { load(); }, [load]);
2628
+ // 默认滚到最底:看日志要看最新一条(tail 语义),加载/刷新后贴底
2629
+ react.useEffect(function () {
2630
+ if (lines && preRef.current) preRef.current.scrollTop = preRef.current.scrollHeight;
2631
+ }, [lines]);
2368
2632
 
2369
2633
  return react.createElement(
2370
2634
  "div", null,
@@ -2380,7 +2644,7 @@ window.__ModuleLoader__.load({
2380
2644
  { style: Object.assign({}, S.error, { marginBottom: 10 }) },
2381
2645
  "日志读取失败:" + error + "(点右上“刷新”重试)",
2382
2646
  )
2383
- : react.createElement("pre", { style: S.pre }, (lines || []).join("\n") || "(暂无日志)"),
2647
+ : react.createElement("pre", { style: S.pre, ref: preRef }, (lines || []).join("\n") || "(暂无日志)"),
2384
2648
  );
2385
2649
  }
2386
2650