dsh-brief-sidebar 0.9.6 → 0.9.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.
package/dsh.plugin.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "id": "dsh-external/dsh-brief-sidebar",
3
- "version": "0.9.6",
3
+ "version": "0.9.7",
4
4
  "main": "./lib/index.mjs",
5
5
  "description": "会话简报侧栏页(brief):把会话的 todo 列表(`todos` 投影的进展看板)、产物列表(`dshSummaryDeliverables` 投影的实时产出文件)与记忆槽位(`memorySlots` 投影,来自 dsh-prime-memory,开关可关)一起渲染成右侧栏的一个「概要」tab,并遮蔽 DSH 官方那条 composer 上方的 todo 面板。只读展示,不做写回。",
6
6
  "engines": {
package/lib/client.js CHANGED
@@ -1622,18 +1622,30 @@ window.__ModuleLoader__.load({
1622
1622
  function readTool(value) {
1623
1623
  return typeof value === "string" && value.length > 0 ? value : "advanced_search";
1624
1624
  }
1625
+ /**
1626
+ * The queries that surfaced a source, newest first.
1627
+ *
1628
+ * Two legacy shapes are repaired rather than dropped: a frame written before
1629
+ * the cluster change carries a single `query` string, and a drifted value
1630
+ * (non-array, or an array of junk) reads as "member of no cluster" — the row
1631
+ * still shows, it just joins the tail group.
1632
+ */
1633
+ function readQueries(value) {
1634
+ if (Array.isArray(value)) return [...new Set(value.filter((q) => typeof q === "string" && q.length > 0))];
1635
+ if (typeof value === "string" && value.length > 0) return [value];
1636
+ return [];
1637
+ }
1625
1638
  /** Narrow one source row; `undefined` = malformed, dropped. */
1626
1639
  function readItem$1(value) {
1627
1640
  if (value === null || typeof value !== "object" || Array.isArray(value)) return void 0;
1628
1641
  const raw = value;
1629
1642
  if (typeof raw["url"] !== "string" || raw["url"].length === 0) return void 0;
1630
1643
  if (typeof raw["host"] !== "string") return void 0;
1631
- if (typeof raw["query"] !== "string") return void 0;
1632
1644
  if (!isCounter$2(raw["count"]) || !isCounter$2(raw["lastTurn"])) return void 0;
1633
1645
  const item = {
1634
1646
  url: raw["url"],
1635
1647
  host: raw["host"],
1636
- query: raw["query"],
1648
+ queries: readQueries(raw["queries"] ?? raw["query"]),
1637
1649
  tool: readTool(raw["tool"]),
1638
1650
  count: raw["count"],
1639
1651
  lastTurn: raw["lastTurn"]
@@ -1699,6 +1711,43 @@ window.__ModuleLoader__.load({
1699
1711
  return view.latest?.urls.includes(urlKey(url)) === true;
1700
1712
  }
1701
1713
  /**
1714
+ * Cluster the result bubbles under the queries that surfaced them.
1715
+ *
1716
+ * Membership is the source's own `queries` list, so a source a merged
1717
+ * multi-query `web_search` brought back appears under each of them — that is
1718
+ * what happened, and hiding the repeat would make the counts under the headers
1719
+ * disagree with the cards on screen. Queries come newest-first from the fold;
1720
+ * a source no listed query claims (its query aged out of the capped list, or a
1721
+ * pre-cluster frame) lands in a tail group rather than disappearing.
1722
+ */
1723
+ function clusterByQuery(view) {
1724
+ const latestTurn = view.latest?.turn;
1725
+ const claimed = /* @__PURE__ */ new Set();
1726
+ const out = [];
1727
+ for (const row of view.queries) {
1728
+ const items = view.sources.filter((item) => item.queries.includes(row.query));
1729
+ for (const item of items) claimed.add(item.url);
1730
+ out.push({
1731
+ query: row.query,
1732
+ tool: row.tool,
1733
+ turn: row.turn,
1734
+ hits: row.hits,
1735
+ items,
1736
+ latest: latestTurn !== void 0 && row.turn === latestTurn
1737
+ });
1738
+ }
1739
+ const rest = view.sources.filter((item) => !claimed.has(item.url));
1740
+ if (rest.length > 0) out.push({
1741
+ query: "",
1742
+ tool: rest[0]?.tool ?? "",
1743
+ turn: rest[0]?.lastTurn ?? 0,
1744
+ hits: rest.length,
1745
+ items: rest,
1746
+ latest: false
1747
+ });
1748
+ return out;
1749
+ }
1750
+ /**
1702
1751
  * The href to render for a source URL, or `undefined` when it must not be a link.
1703
1752
  *
1704
1753
  * Search results are untrusted external data: a `javascript:` or `data:` URL in
@@ -2230,6 +2279,7 @@ window.__ModuleLoader__.load({
2230
2279
  function SearchRefSection(props) {
2231
2280
  const { t, ctx, scope, hideHeader = false } = props;
2232
2281
  const view = readSearchRefs(useProjectionValue(ctx, scope?.sessionId, SEARCH_REFS_KEY));
2282
+ const [openMap, setOpenMap] = (0, react.useState)({});
2233
2283
  if (view === void 0) return null;
2234
2284
  if (view.queries.length === 0 && view.sources.length === 0) return (0, react.createElement)("div", {
2235
2285
  style: SECTION_STYLE$3,
@@ -2239,10 +2289,8 @@ window.__ModuleLoader__.load({
2239
2289
  color: "var(--dsw-alias-label-secondary)"
2240
2290
  } }, t("searchRefs.empty")), (0, react.createElement)("div", null, t("searchRefs.emptyHint"))));
2241
2291
  const body = [];
2242
- if (view.sources.length > 0) body.push((0, react.createElement)("ul", {
2243
- key: "sl",
2244
- style: LIST_STYLE$2
2245
- }, view.sources.map((item) => {
2292
+ /** One result bubble; shared by every cluster so the cards stay identical. */
2293
+ const renderBubble = (item) => {
2246
2294
  const href = safeHref(item.url);
2247
2295
  const label = item.title ?? item.url;
2248
2296
  const link = href === void 0 ? (0, react.createElement)("span", { style: TITLE_STYLE$2 }, label) : (0, react.createElement)("a", {
@@ -2260,7 +2308,29 @@ window.__ModuleLoader__.load({
2260
2308
  style: BUBBLE_STYLE$1,
2261
2309
  "data-dsh-brief-sidebar": "search-refs-bubble"
2262
2310
  }, link, (0, react.createElement)("span", { style: HOST_STYLE }, item.host), item.snippet !== void 0 && (0, react.createElement)("p", { style: SNIPPET_STYLE }, item.snippet), (latest || item.count > 1) && (0, react.createElement)("div", { style: BUBBLE_FOOT_STYLE }, latest && (0, react.createElement)("span", { style: TAG_STYLE }, t("searchRefs.latestTag")), item.count > 1 && (0, react.createElement)("span", { style: HOST_STYLE }, `\u00d7${item.count}`))));
2263
- })));
2311
+ };
2312
+ if (view.sources.length > 0) for (const cluster of clusterByQuery(view)) {
2313
+ const key = cluster.query === "" ? "\0tail" : cluster.query;
2314
+ const open = openMap[key] ?? cluster.latest;
2315
+ body.push((0, react.createElement)(FoldToggle, {
2316
+ key: `h:${key}`,
2317
+ label: cluster.query === "" ? expandCount(t("searchRefs.sourceGroup"), cluster.items.length) : `${cluster.query} · ${expandCount(t("searchRefs.hits"), cluster.hits)}`,
2318
+ open,
2319
+ marker: "search-refs-cluster",
2320
+ onToggle: () => {
2321
+ setOpenMap((prev) => ({
2322
+ ...prev,
2323
+ [key]: !open
2324
+ }));
2325
+ }
2326
+ }));
2327
+ if (!open) continue;
2328
+ body.push((0, react.createElement)("ul", {
2329
+ key: `b:${key}`,
2330
+ style: LIST_STYLE$2,
2331
+ "data-dsh-brief-sidebar": "search-refs-cluster-items"
2332
+ }, cluster.items.map(renderBubble)));
2333
+ }
2264
2334
  else body.push((0, react.createElement)("div", {
2265
2335
  key: "ns",
2266
2336
  style: FOOTER_STYLE$2,