dsh-brief-sidebar 0.9.5 → 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.5",
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
@@ -253,7 +253,7 @@ window.__ModuleLoader__.load({
253
253
  display: "flex",
254
254
  flexDirection: "column"
255
255
  };
256
- const ROW_STYLE$6 = {
256
+ const ROW_STYLE$5 = {
257
257
  display: "flex",
258
258
  alignItems: "flex-start",
259
259
  gap: 8,
@@ -305,7 +305,7 @@ window.__ModuleLoader__.load({
305
305
  /** One task row. */
306
306
  function TodoRow(props) {
307
307
  const { item, t } = props;
308
- return (0, react.createElement)("li", { style: ROW_STYLE$6 }, (0, react.createElement)("span", { style: pillStyle(item.status) }, t(statusKey(item.status))), (0, react.createElement)("span", { style: contentStyle(item.status) }, item.content));
308
+ return (0, react.createElement)("li", { style: ROW_STYLE$5 }, (0, react.createElement)("span", { style: pillStyle(item.status) }, t(statusKey(item.status))), (0, react.createElement)("span", { style: contentStyle(item.status) }, item.content));
309
309
  }
310
310
  /**
311
311
  * The progress section. The subscription is unconditional (a plain listener,
@@ -1363,7 +1363,7 @@ window.__ModuleLoader__.load({
1363
1363
  display: "flex",
1364
1364
  flexDirection: "column"
1365
1365
  };
1366
- const ROW_STYLE$5 = {
1366
+ const ROW_STYLE$4 = {
1367
1367
  display: "flex",
1368
1368
  alignItems: "flex-start",
1369
1369
  gap: 8,
@@ -1409,11 +1409,11 @@ window.__ModuleLoader__.load({
1409
1409
  const title = plan === null ? path : `${t("deliverable.codeplanTag")}: ${plan.task} · ${path}`;
1410
1410
  const label = basename(path);
1411
1411
  const content = plan === null ? null : (0, react.createElement)("span", { style: TAG_STYLE$2 }, t("deliverable.codeplanTag"));
1412
- if (open === void 0) return (0, react.createElement)("li", { style: ROW_STYLE$5 }, content, (0, react.createElement)("span", {
1412
+ if (open === void 0) return (0, react.createElement)("li", { style: ROW_STYLE$4 }, content, (0, react.createElement)("span", {
1413
1413
  style: PATH_STYLE,
1414
1414
  title
1415
1415
  }, label));
1416
- return (0, react.createElement)("li", { style: ROW_STYLE$5 }, content, (0, react.createElement)("button", {
1416
+ return (0, react.createElement)("li", { style: ROW_STYLE$4 }, content, (0, react.createElement)("button", {
1417
1417
  style: BUTTON_STYLE,
1418
1418
  title,
1419
1419
  onClick: () => {
@@ -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
@@ -2004,7 +2053,7 @@ window.__ModuleLoader__.load({
2004
2053
  display: "flex",
2005
2054
  flexDirection: "column"
2006
2055
  };
2007
- const ROW_STYLE$4 = {
2056
+ const ROW_STYLE$3 = {
2008
2057
  display: "flex",
2009
2058
  alignItems: "baseline",
2010
2059
  gap: 8,
@@ -2072,7 +2121,7 @@ window.__ModuleLoader__.load({
2072
2121
  marker: "skill-usage-invoked-group"
2073
2122
  }), (0, react.createElement)("ul", { style: LIST_STYLE$3 }, view.session.map((entry) => (0, react.createElement)("li", {
2074
2123
  key: entry.name,
2075
- style: ROW_STYLE$4,
2124
+ style: ROW_STYLE$3,
2076
2125
  "data-dsh-brief-sidebar": "skill-usage-row"
2077
2126
  }, (0, react.createElement)("span", { style: NAME_STYLE$1 }, entry.name), (0, react.createElement)("span", { style: COUNT_STYLE }, expandPair(t("skillUsage.uses"), entry.count, entry.count)), latestNames.includes(entry.name) && (0, react.createElement)("span", { style: TAG_STYLE$1 }, t("skillUsage.latestTag"))))), rest.length > 0 && (0, react.createElement)(FoldToggle, {
2078
2127
  label: expandCount(t("skillUsage.availableGroup"), rest.length),
@@ -2086,7 +2135,7 @@ window.__ModuleLoader__.load({
2086
2135
  marker: "skill-usage-available-capped"
2087
2136
  }), availableOpen && (0, react.createElement)("ul", { style: LIST_STYLE$3 }, rest.map((name) => (0, react.createElement)("li", {
2088
2137
  key: name,
2089
- style: ROW_STYLE$4,
2138
+ style: ROW_STYLE$3,
2090
2139
  "data-dsh-brief-sidebar": "skill-usage-available-row"
2091
2140
  }, (0, react.createElement)("span", { style: NAME_STYLE$1 }, name)))), view.session.length > 0 && (0, react.createElement)("div", { style: FOOTER_STYLE$3 }, expandPair(t("skillUsage.count"), view.callTotal, view.sessionTotal)));
2092
2141
  }
@@ -2096,15 +2145,19 @@ window.__ModuleLoader__.load({
2096
2145
  * The search-reference section: the `dshSummarySearchRefs` projection drawn as
2097
2146
  * one tab of the references section.
2098
2147
  *
2099
- * It answers "what did this session pull in from outside": the queries it ran
2100
- * — via the harness's `web_search` or the user's own `advanced_search` — and the
2101
- * sources those searches returned. Each query row carries its tool name, because
2102
- * which of the two answered is a fact the reader asks about. Two groups, because
2103
- * they are two different facts — a query says what was asked, a source says what
2104
- * came back, and an older `dsh-free-search` without `presentationMeta` produces
2105
- * queries with no sources at all. That degraded case is stated rather than
2106
- * hidden, because an empty source list would otherwise read as "the search found
2107
- * nothing".
2148
+ * It answers "what did this session pull in from outside", and it answers it the
2149
+ * way the conversation to its left does: **one left-aligned bubble per source**,
2150
+ * title as a link, host and provider snippet under it. No group headers, no list
2151
+ * of query titles — that wall duplicated what the transcript already shows, and a
2152
+ * reader who opens this tab is looking for the results, not for a restatement of
2153
+ * the questions.
2154
+ *
2155
+ * The fold still records which query and which tool (`web_search` or the user's
2156
+ * own `advanced_search`) surfaced each source; that data stays in the frame for
2157
+ * any surface that wants it, it is just no longer drawn here. An older
2158
+ * `dsh-free-search` without `presentationMeta` yields queries with no sources —
2159
+ * that degraded case is stated (`searchRefs.noSourcesHint`) rather than left as
2160
+ * an empty pane, because an empty list reads as "the search found nothing".
2108
2161
  *
2109
2162
  * Optional on both sides: no registry, or a host half that never registered the
2110
2163
  * unit, means the client reads capability absence and the section hides.
@@ -2130,31 +2183,45 @@ window.__ModuleLoader__.load({
2130
2183
  const LIST_STYLE$2 = {
2131
2184
  listStyle: "none",
2132
2185
  margin: 0,
2133
- padding: "6px 0",
2134
- display: "flex",
2135
- flexDirection: "column"
2136
- };
2137
- const ROW_STYLE$3 = {
2186
+ padding: "8px 12px 10px",
2138
2187
  display: "flex",
2188
+ flexDirection: "column",
2139
2189
  alignItems: "flex-start",
2140
- gap: 8,
2141
- padding: "6px 12px"
2190
+ gap: 8
2142
2191
  };
2143
- const QUERY_STYLE = {
2192
+ /**
2193
+ * One result, drawn as a chat bubble: left-aligned, background-filled, rounded,
2194
+ * and narrower than the column. The reader reads this tab the way they read the
2195
+ * conversation to its left — each bubble is one thing the session pulled in — so
2196
+ * the list chrome (row rules, group headers, per-query title lines) is dropped
2197
+ * rather than restyled.
2198
+ *
2199
+ * `alignItems: flex-start` on the list is what keeps bubbles hugging the left
2200
+ * edge; without it a flex child stretches to the full column and the bubble
2201
+ * reads as a plain card.
2202
+ */
2203
+ const BUBBLE_STYLE$1 = {
2204
+ maxWidth: "92%",
2205
+ padding: "8px 10px",
2206
+ borderRadius: 10,
2207
+ background: "var(--dsw-alias-bg-layer-3)",
2208
+ border: "1px solid var(--dsw-alias-border-l2)",
2209
+ color: "var(--dsw-alias-label-primary)",
2210
+ fontSize: 12,
2211
+ lineHeight: "17px",
2144
2212
  minWidth: 0,
2145
- flex: "1 1 auto",
2146
- overflowWrap: "anywhere",
2147
- color: "var(--dsw-alias-label-secondary)"
2213
+ display: "flex",
2214
+ flexDirection: "column",
2215
+ gap: 2
2148
2216
  };
2149
- /** The source title, one line: the host and URL carry the identification. */
2217
+ /** The source title, up to three lines: inside a bubble it IS the content. */
2150
2218
  const TITLE_STYLE$2 = {
2151
2219
  minWidth: 0,
2152
- flex: "1 1 auto",
2220
+ overflowWrap: "anywhere",
2153
2221
  display: "-webkit-box",
2154
- WebkitLineClamp: 2,
2222
+ WebkitLineClamp: 3,
2155
2223
  WebkitBoxOrient: "vertical",
2156
- overflow: "hidden",
2157
- color: "var(--dsw-alias-label-secondary)"
2224
+ overflow: "hidden"
2158
2225
  };
2159
2226
  const SNIPPET_STYLE = {
2160
2227
  margin: 0,
@@ -2164,12 +2231,19 @@ window.__ModuleLoader__.load({
2164
2231
  fontSize: 11,
2165
2232
  lineHeight: "16px"
2166
2233
  };
2167
- const META_STYLE$2 = {
2168
- flexShrink: 0,
2234
+ const HOST_STYLE = {
2169
2235
  color: "var(--dsw-alias-label-tertiary)",
2170
2236
  fontSize: 11,
2171
- lineHeight: "18px",
2172
- whiteSpace: "nowrap"
2237
+ lineHeight: "16px",
2238
+ overflowWrap: "anywhere"
2239
+ };
2240
+ /** The bubble's own footer strip — where the "本轮" pill lives now. */
2241
+ const BUBBLE_FOOT_STYLE = {
2242
+ display: "flex",
2243
+ alignItems: "center",
2244
+ gap: 6,
2245
+ marginTop: 2,
2246
+ minWidth: 0
2173
2247
  };
2174
2248
  /** The "本轮" pill: same form as the skill tag, tertiary emphasis. */
2175
2249
  const TAG_STYLE = {
@@ -2205,6 +2279,7 @@ window.__ModuleLoader__.load({
2205
2279
  function SearchRefSection(props) {
2206
2280
  const { t, ctx, scope, hideHeader = false } = props;
2207
2281
  const view = readSearchRefs(useProjectionValue(ctx, scope?.sessionId, SEARCH_REFS_KEY));
2282
+ const [openMap, setOpenMap] = (0, react.useState)({});
2208
2283
  if (view === void 0) return null;
2209
2284
  if (view.queries.length === 0 && view.sources.length === 0) return (0, react.createElement)("div", {
2210
2285
  style: SECTION_STYLE$3,
@@ -2214,52 +2289,49 @@ window.__ModuleLoader__.load({
2214
2289
  color: "var(--dsw-alias-label-secondary)"
2215
2290
  } }, t("searchRefs.empty")), (0, react.createElement)("div", null, t("searchRefs.emptyHint"))));
2216
2291
  const body = [];
2217
- if (view.queries.length > 0) {
2218
- body.push((0, react.createElement)(GroupHeader, {
2219
- key: "qh",
2220
- text: expandCount(t("searchRefs.queryGroup"), view.queries.length),
2221
- marker: "search-refs-query-group"
2292
+ /** One result bubble; shared by every cluster so the cards stay identical. */
2293
+ const renderBubble = (item) => {
2294
+ const href = safeHref(item.url);
2295
+ const label = item.title ?? item.url;
2296
+ const link = href === void 0 ? (0, react.createElement)("span", { style: TITLE_STYLE$2 }, label) : (0, react.createElement)("a", {
2297
+ style: TITLE_STYLE$2,
2298
+ href,
2299
+ target: "_blank",
2300
+ rel: "noreferrer noopener"
2301
+ }, label);
2302
+ const latest = isLatestSource(view, item.url);
2303
+ return (0, react.createElement)("li", {
2304
+ key: item.url,
2305
+ style: { minWidth: 0 },
2306
+ "data-dsh-brief-sidebar": "search-refs-source"
2307
+ }, (0, react.createElement)("div", {
2308
+ style: BUBBLE_STYLE$1,
2309
+ "data-dsh-brief-sidebar": "search-refs-bubble"
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}`))));
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
+ }
2222
2326
  }));
2327
+ if (!open) continue;
2223
2328
  body.push((0, react.createElement)("ul", {
2224
- key: "ql",
2225
- style: LIST_STYLE$2
2226
- }, view.queries.map((row) => (0, react.createElement)("li", {
2227
- key: `${row.turn}:${row.query}`,
2228
- style: ROW_STYLE$3,
2229
- "data-dsh-brief-sidebar": "search-refs-query"
2230
- }, (0, react.createElement)("span", { style: QUERY_STYLE }, row.query), (0, react.createElement)("span", { style: TAG_STYLE }, row.tool), (0, react.createElement)("span", { style: META_STYLE$2 }, expandCount(t("searchRefs.hits"), row.hits))))));
2329
+ key: `b:${key}`,
2330
+ style: LIST_STYLE$2,
2331
+ "data-dsh-brief-sidebar": "search-refs-cluster-items"
2332
+ }, cluster.items.map(renderBubble)));
2231
2333
  }
2232
- if (view.sources.length > 0) {
2233
- body.push((0, react.createElement)(GroupHeader, {
2234
- key: "sh",
2235
- text: expandCount(t("searchRefs.sourceGroup"), view.sources.length),
2236
- marker: "search-refs-source-group"
2237
- }));
2238
- body.push((0, react.createElement)("ul", {
2239
- key: "sl",
2240
- style: LIST_STYLE$2
2241
- }, view.sources.map((item) => {
2242
- const href = safeHref(item.url);
2243
- const label = item.title ?? item.url;
2244
- const link = href === void 0 ? (0, react.createElement)("span", { style: TITLE_STYLE$2 }, label) : (0, react.createElement)("a", {
2245
- style: TITLE_STYLE$2,
2246
- href,
2247
- target: "_blank",
2248
- rel: "noreferrer noopener"
2249
- }, label);
2250
- return (0, react.createElement)("li", {
2251
- key: item.url,
2252
- style: ROW_STYLE$3,
2253
- "data-dsh-brief-sidebar": "search-refs-source"
2254
- }, (0, react.createElement)("div", { style: {
2255
- minWidth: 0,
2256
- flex: "1 1 auto",
2257
- display: "flex",
2258
- flexDirection: "column",
2259
- gap: 2
2260
- } }, link, (0, react.createElement)("span", { style: META_STYLE$2 }, item.host), item.snippet !== void 0 && (0, react.createElement)("p", { style: SNIPPET_STYLE }, item.snippet)), isLatestSource(view, item.url) && (0, react.createElement)("span", { style: TAG_STYLE }, t("searchRefs.latestTag")));
2261
- })));
2262
- } else body.push((0, react.createElement)("div", {
2334
+ else body.push((0, react.createElement)("div", {
2263
2335
  key: "ns",
2264
2336
  style: FOOTER_STYLE$2,
2265
2337
  "data-dsh-brief-sidebar": "search-refs-no-sources"