dsh-brief-sidebar 0.9.2 → 0.9.4

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/index.mjs CHANGED
@@ -5375,7 +5375,7 @@ function superRefine(fn, params) {
5375
5375
  * lists are capped; the uncapped `sessionTotal` keeps the summary truthful
5376
5376
  * beyond the cap.
5377
5377
  */
5378
- const CAPS$1 = {
5378
+ const CAPS$2 = {
5379
5379
  /** Latest-turn paths kept in the wire view. */
5380
5380
  latest: 50,
5381
5381
  /** Session-wide paths kept in the wire view (most recent win). */
@@ -5442,10 +5442,10 @@ function applyDeliverablesEvent(state, event) {
5442
5442
  const latest = state.latest;
5443
5443
  const nextLatest = latest !== null && latest.turn === recorded.turn && !latest.paths.includes(path) ? {
5444
5444
  turn: latest.turn,
5445
- paths: [...latest.paths, path].slice(-CAPS$1.latest)
5445
+ paths: [...latest.paths, path].slice(-CAPS$2.latest)
5446
5446
  } : latest;
5447
5447
  const withoutDuplicate = state.sessionPaths.filter((existing) => existing !== path);
5448
- const sessionPaths = [path, ...withoutDuplicate].slice(0, CAPS$1.session);
5448
+ const sessionPaths = [path, ...withoutDuplicate].slice(0, CAPS$2.session);
5449
5449
  const sessionTotal = withoutDuplicate.length === state.sessionPaths.length ? state.sessionTotal + 1 : state.sessionTotal;
5450
5450
  return {
5451
5451
  ...state,
@@ -5509,7 +5509,7 @@ function isRecord(value) {
5509
5509
  * Capacity bounds. The unit rides every committed event, so the lists are
5510
5510
  * capped; `sessionTotal`/`callTotal` keep the summary truthful beyond the cap.
5511
5511
  */
5512
- const CAPS = {
5512
+ const CAPS$1 = {
5513
5513
  /** Distinct skills kept in state (most recent win). */
5514
5514
  session: 50,
5515
5515
  /** Skills kept in the latest-turn bucket of the wire view. */
@@ -5570,7 +5570,7 @@ function tally(state, skill, turn) {
5570
5570
  name: skill,
5571
5571
  count: (existing?.count ?? 0) + 1,
5572
5572
  lastTurn: turn
5573
- }, ...withoutSkill].slice(0, CAPS.session);
5573
+ }, ...withoutSkill].slice(0, CAPS$1.session);
5574
5574
  const sessionTotal = existing === void 0 ? state.sessionTotal + 1 : state.sessionTotal;
5575
5575
  const latest = state.latest;
5576
5576
  const nextLatest = latest !== null && latest.turn === turn && !latest.names.includes(skill) ? {
@@ -5647,7 +5647,7 @@ function applySkillUsageEvent(state, event) {
5647
5647
  }
5648
5648
  return {
5649
5649
  ...state,
5650
- catalog: names.slice(0, CAPS.available),
5650
+ catalog: names.slice(0, CAPS$1.available),
5651
5651
  catalogCount: names.length
5652
5652
  };
5653
5653
  }
@@ -5663,6 +5663,199 @@ function applySkillUsageEvent(state, event) {
5663
5663
  return tally(state, recorded.skill, recorded.turn);
5664
5664
  }
5665
5665
  //#endregion
5666
+ //#region src/projection/search-ref-fold.ts
5667
+ /**
5668
+ * Capacity bounds. `snippet` is capped because it is the only long text in the
5669
+ * state and the frame is republished per event; the provider already caps it
5670
+ * once in its own `presentationMeta`, so this is a second, tighter belt.
5671
+ */
5672
+ const CAPS = {
5673
+ /** Distinct sources kept in state. */
5674
+ items: 20,
5675
+ /** Distinct queries kept in state. */
5676
+ queries: 20,
5677
+ /** URLs kept in the latest-turn bucket. */
5678
+ latest: 20,
5679
+ /** Snippet characters kept per source. */
5680
+ snippet: 160
5681
+ };
5682
+ /** The fold's initial state: no search has settled yet. */
5683
+ function initSearchRefState() {
5684
+ return {
5685
+ turn: 0,
5686
+ calls: {},
5687
+ latest: null,
5688
+ items: [],
5689
+ queries: [],
5690
+ sourceTotal: 0,
5691
+ queryTotal: 0
5692
+ };
5693
+ }
5694
+ /** The tool this fold owns. Nothing else counts, even with a `query` argument. */
5695
+ const SEARCH_TOOL = "advanced_search";
5696
+ /**
5697
+ * Extract the query string from one committed `tool/call`.
5698
+ *
5699
+ * @param name - wire tool name.
5700
+ * @param argsRaw - model-produced JSON arguments (a string by the session contract).
5701
+ * @returns the non-blank query, or null when this call is not a search.
5702
+ */
5703
+ function queryFromArgs(name, argsRaw) {
5704
+ if (name !== SEARCH_TOOL) return null;
5705
+ let args;
5706
+ try {
5707
+ args = JSON.parse(argsRaw);
5708
+ } catch {
5709
+ return null;
5710
+ }
5711
+ if (typeof args !== "object" || args === null || Array.isArray(args)) return null;
5712
+ const query = args["query"];
5713
+ return typeof query === "string" && query.trim().length > 0 ? query : null;
5714
+ }
5715
+ /**
5716
+ * The dedupe key. Host case and a trailing slash are not meaningful for
5717
+ * identity; path case IS, so it is left alone. Exported because the client
5718
+ * matches a row against the latest-turn bucket with the same algorithm.
5719
+ */
5720
+ function urlKey(url) {
5721
+ const trimmed = url.trim();
5722
+ return (trimmed.endsWith("/") ? trimmed.slice(0, -1) : trimmed).replace(/^[a-z][a-z0-9+.-]*:\/\/[^/]+/i, (match) => match.toLowerCase());
5723
+ }
5724
+ /** The host label of a URL, or the raw string when it does not parse. */
5725
+ function hostOf(url) {
5726
+ try {
5727
+ return new URL(url).hostname.replace(/^www\./i, "");
5728
+ } catch {
5729
+ return url.slice(0, 40);
5730
+ }
5731
+ }
5732
+ /** Cap one snippet so one long provider blurb cannot dominate every frame. */
5733
+ function capSnippet(text) {
5734
+ const one = text.replace(/\s+/g, " ").trim();
5735
+ return one.length > CAPS.snippet ? `${one.slice(0, CAPS.snippet)}…` : one;
5736
+ }
5737
+ /** The sources of one result meta, or `[]` when the meta is absent or drifted. */
5738
+ function readSources(meta) {
5739
+ if (typeof meta !== "object" || meta === null) return [];
5740
+ const raw = meta["sources"];
5741
+ if (!Array.isArray(raw)) return [];
5742
+ const out = [];
5743
+ for (const entry of raw) {
5744
+ if (typeof entry !== "object" || entry === null) continue;
5745
+ const record = entry;
5746
+ if (typeof record["url"] !== "string" || record["url"].trim().length === 0) continue;
5747
+ const source = { url: record["url"] };
5748
+ if (typeof record["title"] === "string" && record["title"].length > 0) source.title = record["title"];
5749
+ if (typeof record["snippet"] === "string" && record["snippet"].length > 0) source.snippet = capSnippet(record["snippet"]);
5750
+ out.push(source);
5751
+ }
5752
+ return out;
5753
+ }
5754
+ /**
5755
+ * The next state after one committed event. Events this fold does not own
5756
+ * return the SAME reference, which is what keeps the registry's `Object.is`
5757
+ * drive from republishing the key on unrelated frames.
5758
+ *
5759
+ * @param state - the state covering all prior events.
5760
+ * @param event - the next committed session event.
5761
+ * @returns the next state (same reference when nothing the fold tracks moved).
5762
+ */
5763
+ function applySearchRefEvent(state, event) {
5764
+ if (event.type === "turn/start") {
5765
+ const turn = event.data;
5766
+ if (typeof turn?.turn !== "number") return state;
5767
+ const calls = {};
5768
+ for (const [id, entry] of Object.entries(state.calls)) if (entry.turn >= turn.turn - 1) calls[id] = entry;
5769
+ return {
5770
+ ...state,
5771
+ turn: turn.turn,
5772
+ calls,
5773
+ latest: {
5774
+ turn: turn.turn,
5775
+ urls: []
5776
+ }
5777
+ };
5778
+ }
5779
+ if (event.type === "tool/call") {
5780
+ const call = event.data;
5781
+ if (typeof call?.callId !== "string") return state;
5782
+ if (typeof call.name !== "string" || typeof call.arguments !== "string") return state;
5783
+ const query = queryFromArgs(call.name, call.arguments);
5784
+ if (query === null) return state;
5785
+ return {
5786
+ ...state,
5787
+ calls: {
5788
+ ...state.calls,
5789
+ [call.callId]: {
5790
+ turn: state.turn,
5791
+ query
5792
+ }
5793
+ }
5794
+ };
5795
+ }
5796
+ if (event.type !== "tool/result") return state;
5797
+ const result = event.data;
5798
+ const callId = result?.message?.source?.callId;
5799
+ if (typeof callId !== "string") return state;
5800
+ if (result?.message?.content?.[0]?.isError === true) return state;
5801
+ const recorded = state.calls[callId];
5802
+ if (recorded === void 0) return state;
5803
+ const { query, turn } = recorded;
5804
+ const sources = readSources(result.meta);
5805
+ const items = [...state.items];
5806
+ let sourceTotal = state.sourceTotal;
5807
+ const urls = [];
5808
+ for (const source of sources) {
5809
+ const key = urlKey(source.url);
5810
+ urls.push(key);
5811
+ const at = items.findIndex((item) => urlKey(item.url) === key);
5812
+ const row = {
5813
+ url: source.url,
5814
+ host: hostOf(source.url),
5815
+ ...source.title !== void 0 ? { title: source.title } : {},
5816
+ ...source.snippet !== void 0 ? { snippet: source.snippet } : {},
5817
+ query,
5818
+ count: at < 0 ? 1 : (items[at]?.count ?? 0) + 1,
5819
+ lastTurn: turn
5820
+ };
5821
+ if (at >= 0) items.splice(at, 1);
5822
+ else sourceTotal += 1;
5823
+ items.unshift(row);
5824
+ }
5825
+ const queries = [...state.queries];
5826
+ const qAt = queries.findIndex((row) => row.query === query);
5827
+ let queryTotal = state.queryTotal;
5828
+ if (qAt >= 0) {
5829
+ const previous = queries[qAt];
5830
+ queries.splice(qAt, 1);
5831
+ queries.unshift({
5832
+ query,
5833
+ turn,
5834
+ hits: (previous?.hits ?? 0) + sources.length
5835
+ });
5836
+ } else {
5837
+ queries.unshift({
5838
+ query,
5839
+ turn,
5840
+ hits: sources.length
5841
+ });
5842
+ queryTotal += 1;
5843
+ }
5844
+ const latest = state.latest;
5845
+ const nextLatest = latest !== null && latest.turn === turn ? {
5846
+ turn: latest.turn,
5847
+ urls: [...urls.reverse(), ...latest.urls].slice(0, CAPS.latest)
5848
+ } : latest;
5849
+ return {
5850
+ ...state,
5851
+ items: items.slice(0, CAPS.items),
5852
+ queries: queries.slice(0, CAPS.queries),
5853
+ sourceTotal,
5854
+ queryTotal,
5855
+ latest: nextLatest
5856
+ };
5857
+ }
5858
+ //#endregion
5666
5859
  //#region src/projection/keys.ts
5667
5860
  /**
5668
5861
  * The projection keys this plugin contributes to the session-projection
@@ -5685,6 +5878,12 @@ const DELIVERABLES_KEY = "dshSummaryDeliverables";
5685
5878
  * to collide silently.
5686
5879
  */
5687
5880
  const SKILL_USAGE_KEY = "dshSummarySkillUsage";
5881
+ /**
5882
+ * The search-reference key (queries and returned sources of this session's
5883
+ * `advanced_search` calls). Same rule as above: the name is ours, so no other
5884
+ * host-side unit can answer for it by accident.
5885
+ */
5886
+ const SEARCH_REFS_KEY = "dshSummarySearchRefs";
5688
5887
  //#endregion
5689
5888
  //#region src/projection/register.ts
5690
5889
  /**
@@ -5740,9 +5939,9 @@ function view(state) {
5740
5939
  const next = {
5741
5940
  latest: state.latest === null ? null : {
5742
5941
  turn: state.latest.turn,
5743
- paths: state.latest.paths.slice(-CAPS$1.latest)
5942
+ paths: state.latest.paths.slice(-CAPS$2.latest)
5744
5943
  },
5745
- sessionPaths: state.sessionPaths.slice(0, CAPS$1.session),
5944
+ sessionPaths: state.sessionPaths.slice(0, CAPS$2.session),
5746
5945
  sessionTotal: state.sessionTotal
5747
5946
  };
5748
5947
  viewMemo.set(state, next);
@@ -5818,12 +6017,12 @@ function skillView(state) {
5818
6017
  const next = {
5819
6018
  latest: state.latest === null ? null : {
5820
6019
  turn: state.latest.turn,
5821
- names: state.latest.names.slice(0, CAPS.latest)
6020
+ names: state.latest.names.slice(0, CAPS$1.latest)
5822
6021
  },
5823
- session: state.items.slice(0, CAPS.view),
6022
+ session: state.items.slice(0, CAPS$1.view),
5824
6023
  sessionTotal: state.sessionTotal,
5825
6024
  callTotal: state.callTotal,
5826
- available: state.catalog.slice(0, CAPS.available),
6025
+ available: state.catalog.slice(0, CAPS$1.available),
5827
6026
  availableCount: state.catalogCount
5828
6027
  };
5829
6028
  skillViewMemo.set(state, next);
@@ -5851,6 +6050,98 @@ function registerSkillUsageProjection(ctx) {
5851
6050
  });
5852
6051
  });
5853
6052
  }
6053
+ /** Zod schema of the search-reference fold state. */
6054
+ const searchStateSchema = object({
6055
+ turn: number(),
6056
+ calls: record(string(), object({
6057
+ turn: number(),
6058
+ query: string()
6059
+ })),
6060
+ latest: object({
6061
+ turn: number(),
6062
+ urls: array(string())
6063
+ }).nullable(),
6064
+ items: array(object({
6065
+ url: string(),
6066
+ host: string(),
6067
+ title: string().optional(),
6068
+ snippet: string().optional(),
6069
+ query: string(),
6070
+ count: number(),
6071
+ lastTurn: number()
6072
+ })),
6073
+ queries: array(object({
6074
+ query: string(),
6075
+ turn: number(),
6076
+ hits: number()
6077
+ })),
6078
+ sourceTotal: number(),
6079
+ queryTotal: number()
6080
+ });
6081
+ /** Zod schema of the search-reference wire view. */
6082
+ const searchViewSchema = object({
6083
+ latest: object({
6084
+ turn: number(),
6085
+ urls: array(string())
6086
+ }).nullable(),
6087
+ queries: array(object({
6088
+ query: string(),
6089
+ turn: number(),
6090
+ hits: number()
6091
+ })),
6092
+ sources: array(object({
6093
+ url: string(),
6094
+ host: string(),
6095
+ title: string().optional(),
6096
+ snippet: string().optional(),
6097
+ query: string(),
6098
+ count: number(),
6099
+ lastTurn: number()
6100
+ })),
6101
+ queryTotal: number(),
6102
+ sourceTotal: number()
6103
+ });
6104
+ /** Same reference-stability memo, for the search-reference unit. */
6105
+ const searchViewMemo = /* @__PURE__ */ new WeakMap();
6106
+ /** State → wire view, reference-stable across unchanged states. */
6107
+ function searchView(state) {
6108
+ const memoized = searchViewMemo.get(state);
6109
+ if (memoized !== void 0) return memoized;
6110
+ const next = {
6111
+ latest: state.latest === null ? null : {
6112
+ turn: state.latest.turn,
6113
+ urls: state.latest.urls.slice(0, CAPS.latest)
6114
+ },
6115
+ queries: state.queries.slice(0, CAPS.queries),
6116
+ sources: state.items.slice(0, CAPS.items),
6117
+ queryTotal: state.queryTotal,
6118
+ sourceTotal: state.sourceTotal
6119
+ };
6120
+ searchViewMemo.set(state, next);
6121
+ return next;
6122
+ }
6123
+ /**
6124
+ * Register the search-reference projection as an optional contribution.
6125
+ *
6126
+ * @param ctx - the host context the plugin's `apply` receives.
6127
+ */
6128
+ function registerSearchRefProjection(ctx) {
6129
+ ctx.inject(["sessionProjections"], (injected) => {
6130
+ const registry = injected.sessionProjections;
6131
+ if (registry === void 0 || typeof registry.register !== "function") return;
6132
+ registry.register({
6133
+ key: SEARCH_REFS_KEY,
6134
+ stateSchema: searchStateSchema,
6135
+ init: () => initSearchRefState(),
6136
+ apply: applySearchRefEvent,
6137
+ wire: {
6138
+ viewSchema: searchViewSchema,
6139
+ view: searchView
6140
+ },
6141
+ stateVersion: 0
6142
+ });
6143
+ });
6144
+ }
5854
6145
  //#endregion
5855
6146
  //#region src/index.ts
5856
6147
  /** Profile row identity. */
@@ -5880,6 +6171,7 @@ const Config = z.object({
5880
6171
  function apply(ctx) {
5881
6172
  registerDeliverablesProjection(ctx);
5882
6173
  registerSkillUsageProjection(ctx);
6174
+ registerSearchRefProjection(ctx);
5883
6175
  }
5884
6176
  //#endregion
5885
6177
  export { Config, apply, name };