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/client.js CHANGED
@@ -231,11 +231,11 @@ window.__ModuleLoader__.load({
231
231
  in_progress: "var(--dsw-alias-state-business-primary)",
232
232
  pending: "var(--dsw-alias-label-tertiary)"
233
233
  };
234
- const SECTION_STYLE$4 = {
234
+ const SECTION_STYLE$6 = {
235
235
  flexShrink: 0,
236
236
  borderBottom: "1px solid var(--dsw-alias-border-secondary)"
237
237
  };
238
- const HEADER_STYLE$5 = {
238
+ const HEADER_STYLE$7 = {
239
239
  padding: "10px 12px 0",
240
240
  color: "var(--dsw-alias-label-secondary)",
241
241
  fontSize: 12,
@@ -246,14 +246,14 @@ window.__ModuleLoader__.load({
246
246
  color: "var(--dsw-alias-label-secondary)",
247
247
  fontSize: 12
248
248
  };
249
- const LIST_STYLE$4 = {
249
+ const LIST_STYLE$5 = {
250
250
  listStyle: "none",
251
251
  margin: 0,
252
252
  padding: "6px 0",
253
253
  display: "flex",
254
254
  flexDirection: "column"
255
255
  };
256
- const ROW_STYLE$5 = {
256
+ const ROW_STYLE$6 = {
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$5 }, (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$6 }, (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,
@@ -316,24 +316,24 @@ window.__ModuleLoader__.load({
316
316
  const { t, ctx, scope } = props;
317
317
  const todos = useTodos(ctx, scope?.sessionId);
318
318
  if (todos === void 0) return (0, react.createElement)("div", {
319
- style: SECTION_STYLE$4,
319
+ style: SECTION_STYLE$6,
320
320
  "data-dsh-brief-sidebar": "progress-section"
321
- }, (0, react.createElement)("div", { style: HEADER_STYLE$5 }, t("section.progress")), (0, react.createElement)(Notice, {
321
+ }, (0, react.createElement)("div", { style: HEADER_STYLE$7 }, t("section.progress")), (0, react.createElement)(Notice, {
322
322
  title: t("board.unavailable"),
323
323
  detail: t("board.unavailableHint")
324
324
  }));
325
325
  if (todos.length === 0) return (0, react.createElement)("div", {
326
- style: SECTION_STYLE$4,
326
+ style: SECTION_STYLE$6,
327
327
  "data-dsh-brief-sidebar": "progress-section"
328
- }, (0, react.createElement)("div", { style: HEADER_STYLE$5 }, t("section.progress")), (0, react.createElement)(Notice, {
328
+ }, (0, react.createElement)("div", { style: HEADER_STYLE$7 }, t("section.progress")), (0, react.createElement)(Notice, {
329
329
  title: t("board.empty"),
330
330
  detail: t("board.emptyHint")
331
331
  }));
332
332
  const counts = countTodos(todos);
333
333
  return (0, react.createElement)("div", {
334
- style: SECTION_STYLE$4,
334
+ style: SECTION_STYLE$6,
335
335
  "data-dsh-brief-sidebar": "progress-section"
336
- }, (0, react.createElement)("div", { style: HEADER_STYLE$5 }, t("section.progress")), (0, react.createElement)("div", { style: PROGRESS_STYLE }, progressLine(counts, t)), (0, react.createElement)("ul", { style: LIST_STYLE$4 }, todos.map((item, index) => (0, react.createElement)(TodoRow, {
336
+ }, (0, react.createElement)("div", { style: HEADER_STYLE$7 }, t("section.progress")), (0, react.createElement)("div", { style: PROGRESS_STYLE }, progressLine(counts, t)), (0, react.createElement)("ul", { style: LIST_STYLE$5 }, todos.map((item, index) => (0, react.createElement)(TodoRow, {
337
337
  key: `${index}:${item.content}`,
338
338
  item,
339
339
  t
@@ -362,6 +362,12 @@ window.__ModuleLoader__.load({
362
362
  * to collide silently.
363
363
  */
364
364
  const SKILL_USAGE_KEY = "dshSummarySkillUsage";
365
+ /**
366
+ * The search-reference key (queries and returned sources of this session's
367
+ * `advanced_search` calls). Same rule as above: the name is ours, so no other
368
+ * host-side unit can answer for it by accident.
369
+ */
370
+ const SEARCH_REFS_KEY = "dshSummarySearchRefs";
365
371
  //#endregion
366
372
  //#region src/client/file-open.ts
367
373
  /** The address scheme every file resource opens with. */
@@ -688,6 +694,9 @@ window.__ModuleLoader__.load({
688
694
  "recall.supersededTag": "已被摘要取代",
689
695
  "recall.noBody": "(本帧没有这条记忆的全文)",
690
696
  "section.skillUsage": "技能引用",
697
+ "section.references": "引用",
698
+ "references.tabSkills": "技能",
699
+ "references.tabMemory": "记忆",
691
700
  "skillUsage.empty": "本会话还没载入过技能",
692
701
  "skillUsage.emptyHint": "模型用 skill 工具载入、或你直接点用某个技能后,技能名会实时出现在这里。",
693
702
  "skillUsage.uses": "{count} 次",
@@ -696,6 +705,16 @@ window.__ModuleLoader__.load({
696
705
  "skillUsage.invokedGroup": "已引用 · {count}",
697
706
  "skillUsage.availableGroup": "本会话可用 · {count}",
698
707
  "skillUsage.availableCapped": "(可用清单共 {count} 条,列表已截断)",
708
+ "section.searchRefs": "搜索引用",
709
+ "references.tabSearch": "搜索",
710
+ "searchRefs.empty": "本会话还没搜索过",
711
+ "searchRefs.emptyHint": "模型调用 advanced_search 之后,查询与取回的外部来源会实时出现在这里。",
712
+ "searchRefs.queryGroup": "查询 · {count}",
713
+ "searchRefs.sourceGroup": "来源 · {count}",
714
+ "searchRefs.hits": "返回 {count} 条",
715
+ "searchRefs.latestTag": "本轮",
716
+ "searchRefs.noSourcesHint": "(该版本未回传来源清单,升级 dsh-free-search 后可见)",
717
+ "searchRefs.capped": "(会话共 {total} 条,列表已截断)",
699
718
  "settings.title": "概要侧栏",
700
719
  "settings.desc": "右侧栏概要 tab 的分区开关(只读展示)。",
701
720
  "settings.memorySlots": "记忆槽位分区",
@@ -748,6 +767,9 @@ window.__ModuleLoader__.load({
748
767
  "recall.supersededTag": "replaced by summary",
749
768
  "recall.noBody": "(this frame carries no full body for this record)",
750
769
  "section.skillUsage": "Skill references",
770
+ "section.references": "References",
771
+ "references.tabSkills": "Skills",
772
+ "references.tabMemory": "Memory",
751
773
  "skillUsage.empty": "No skills loaded in this session",
752
774
  "skillUsage.emptyHint": "Skills show up here once the model loads one with the `skill` tool, or you invoke one directly.",
753
775
  "skillUsage.uses": "{count}x",
@@ -756,6 +778,16 @@ window.__ModuleLoader__.load({
756
778
  "skillUsage.invokedGroup": "Referenced · {count}",
757
779
  "skillUsage.availableGroup": "Available this session · {count}",
758
780
  "skillUsage.availableCapped": "({count} skills in the catalog; the list is capped)",
781
+ "section.searchRefs": "Web searches",
782
+ "references.tabSearch": "Searches",
783
+ "searchRefs.empty": "No searches in this session",
784
+ "searchRefs.emptyHint": "Queries and the external sources they returned appear here once the model calls advanced_search.",
785
+ "searchRefs.queryGroup": "Queries · {count}",
786
+ "searchRefs.sourceGroup": "Sources · {count}",
787
+ "searchRefs.hits": "{count} returned",
788
+ "searchRefs.latestTag": "This turn",
789
+ "searchRefs.noSourcesHint": "(this dsh-free-search build returns no source list; upgrade to see sources)",
790
+ "searchRefs.capped": "({total} in the session; the list is capped)",
759
791
  "settings.title": "Brief sidebar",
760
792
  "settings.desc": "Section switches for the summary tab in the right sidebar (read-only views).",
761
793
  "settings.memorySlots": "Memory slots section",
@@ -808,6 +840,9 @@ window.__ModuleLoader__.load({
808
840
  "recall.supersededTag": "要約に置換済み",
809
841
  "recall.noBody": "このフレームには全文が含まれていない",
810
842
  "section.skillUsage": "スキル参照",
843
+ "section.references": "参照",
844
+ "references.tabSkills": "スキル",
845
+ "references.tabMemory": "メモリ",
811
846
  "skillUsage.empty": "このセッションではスキルを読み込んでいません",
812
847
  "skillUsage.emptyHint": "モデルが skill ツールで読み込むか、あなたがスキルを直接起動すると、ここにリアルタイムで表示されます。",
813
848
  "skillUsage.uses": "{count} 回",
@@ -816,6 +851,16 @@ window.__ModuleLoader__.load({
816
851
  "skillUsage.invokedGroup": "参照済み · {count}",
817
852
  "skillUsage.availableGroup": "このセッションで利用可能 · {count}",
818
853
  "skillUsage.availableCapped": "(カタログは全 {count} 件、一覧は上限で省略)",
854
+ "section.searchRefs": "Web 検索",
855
+ "references.tabSearch": "検索",
856
+ "searchRefs.empty": "このセッションではまだ検索していません",
857
+ "searchRefs.emptyHint": "モデルが advanced_search を呼び出すと、クエリと取得した外部ソースがここに表示されます。",
858
+ "searchRefs.queryGroup": "クエリ · {count}",
859
+ "searchRefs.sourceGroup": "ソース · {count}",
860
+ "searchRefs.hits": "{count} 件返却",
861
+ "searchRefs.latestTag": "今回のターン",
862
+ "searchRefs.noSourcesHint": "(この dsh-free-search ビルドはソース一覧を返しません。アップグレードすると表示されます)",
863
+ "searchRefs.capped": "(セッション全体で {total} 件、一覧は上限で省略)",
819
864
  "settings.title": "概要サイドバー",
820
865
  "settings.desc": "右サイドバー概要タブのセクション表示切り替え(読み取り専用)。",
821
866
  "settings.memorySlots": "メモリスロットセクション",
@@ -868,6 +913,9 @@ window.__ModuleLoader__.load({
868
913
  "recall.supersededTag": "요약에 대체됨",
869
914
  "recall.noBody": "이 프레임에는 이 기록의 전문이 없습니다",
870
915
  "section.skillUsage": "스킬 참조",
916
+ "section.references": "참조",
917
+ "references.tabSkills": "스킬",
918
+ "references.tabMemory": "메모리",
871
919
  "skillUsage.empty": "이 세션에서 스킬을 불러오지 않았습니다",
872
920
  "skillUsage.emptyHint": "모델이 skill 도구로 불러오거나 당신이 스킬을 직접 실행하면 여기에 실시간으로 나타납니다.",
873
921
  "skillUsage.uses": "{count}회",
@@ -876,6 +924,16 @@ window.__ModuleLoader__.load({
876
924
  "skillUsage.invokedGroup": "참조됨 · {count}",
877
925
  "skillUsage.availableGroup": "이 세션에서 사용 가능 · {count}",
878
926
  "skillUsage.availableCapped": "(카탈로그 전체 {count}개, 목록은 잘림)",
927
+ "section.searchRefs": "웹 검색",
928
+ "references.tabSearch": "검색",
929
+ "searchRefs.empty": "이 세션에서는 아직 검색하지 않았습니다",
930
+ "searchRefs.emptyHint": "모델이 advanced_search를 호출하면 쿼리와 가져온 외부 소스가 여기에 표시됩니다.",
931
+ "searchRefs.queryGroup": "쿼리 · {count}",
932
+ "searchRefs.sourceGroup": "소스 · {count}",
933
+ "searchRefs.hits": "{count}件 반환",
934
+ "searchRefs.latestTag": "이번 턴",
935
+ "searchRefs.noSourcesHint": "(이 dsh-free-search 빌드는 소스 목록을 반환하지 않습니다. 업그레이드 필요)",
936
+ "searchRefs.capped": "(세션 전체 {total}건, 목록은 잘림)",
879
937
  "settings.title": "요약 사이드바",
880
938
  "settings.desc": "오른쪽 사이드바 요약 탭의 섹션 표시 전환(읽기 전용).",
881
939
  "settings.memorySlots": "메모리 슬롯 섹션",
@@ -928,6 +986,9 @@ window.__ModuleLoader__.load({
928
986
  "recall.supersededTag": "remplacé par le résumé",
929
987
  "recall.noBody": "(aucun texte complet pour cette entrée dans cette trame)",
930
988
  "section.skillUsage": "Références de compétences",
989
+ "section.references": "Références",
990
+ "references.tabSkills": "Compétences",
991
+ "references.tabMemory": "Mémoire",
931
992
  "skillUsage.empty": "Aucune compétence chargée dans cette session",
932
993
  "skillUsage.emptyHint": "Les compétences apparaissent ici dès que le modèle en charge une via l’outil `skill`, ou quand vous en lancez une directement.",
933
994
  "skillUsage.uses": "{count}×",
@@ -936,6 +997,16 @@ window.__ModuleLoader__.load({
936
997
  "skillUsage.invokedGroup": "Référencées · {count}",
937
998
  "skillUsage.availableGroup": "Disponibles dans cette session · {count}",
938
999
  "skillUsage.availableCapped": "({count} compétences au catalogue ; la liste est tronquée)",
1000
+ "section.searchRefs": "Recherches web",
1001
+ "references.tabSearch": "Recherches",
1002
+ "searchRefs.empty": "Aucune recherche dans cette session",
1003
+ "searchRefs.emptyHint": "Les requêtes et les sources externes renvoyées apparaissent ici dès que le modèle appelle advanced_search.",
1004
+ "searchRefs.queryGroup": "Requêtes · {count}",
1005
+ "searchRefs.sourceGroup": "Sources · {count}",
1006
+ "searchRefs.hits": "{count} renvoyées",
1007
+ "searchRefs.latestTag": "Ce tour-ci",
1008
+ "searchRefs.noSourcesHint": "(cette version de dsh-free-search ne renvoie pas la liste des sources ; mettez à jour)",
1009
+ "searchRefs.capped": "({total} dans la session ; la liste est tronquée)",
939
1010
  "settings.title": "Barre latérale Résumé",
940
1011
  "settings.desc": "Interrupteurs des sections de l’onglet Résumé de la barre latérale (lecture seule).",
941
1012
  "settings.memorySlots": "Section des emplacements mémoire",
@@ -988,6 +1059,9 @@ window.__ModuleLoader__.load({
988
1059
  "recall.supersededTag": "durch Zusammenfassung ersetzt",
989
1060
  "recall.noBody": "(dieser Rahmen enthält keinen vollständigen Text für diesen Eintrag)",
990
1061
  "section.skillUsage": "Skill-Referenzen",
1062
+ "section.references": "Referenzen",
1063
+ "references.tabSkills": "Skills",
1064
+ "references.tabMemory": "Speicher",
991
1065
  "skillUsage.empty": "In dieser Sitzung wurden keine Skills geladen",
992
1066
  "skillUsage.emptyHint": "Skills erscheinen hier, sobald das Modell einen über das `skill`-Tool lädt oder du einen direkt aufrufst.",
993
1067
  "skillUsage.uses": "{count}x",
@@ -996,6 +1070,16 @@ window.__ModuleLoader__.load({
996
1070
  "skillUsage.invokedGroup": "Referenziert · {count}",
997
1071
  "skillUsage.availableGroup": "In dieser Sitzung verfügbar · {count}",
998
1072
  "skillUsage.availableCapped": "({count} Skills im Katalog; die Liste ist gekürzt)",
1073
+ "section.searchRefs": "Websuchen",
1074
+ "references.tabSearch": "Suchen",
1075
+ "searchRefs.empty": "Keine Suchen in dieser Sitzung",
1076
+ "searchRefs.emptyHint": "Abfragen und die zurückgegebenen externen Quellen erscheinen hier, sobald das Modell advanced_search aufruft.",
1077
+ "searchRefs.queryGroup": "Abfragen · {count}",
1078
+ "searchRefs.sourceGroup": "Quellen · {count}",
1079
+ "searchRefs.hits": "{count} zurückgegeben",
1080
+ "searchRefs.latestTag": "Dieser Durchlauf",
1081
+ "searchRefs.noSourcesHint": "(dieser dsh-free-search-Build liefert keine Quellenliste; aktualisieren, um Quellen zu sehen)",
1082
+ "searchRefs.capped": "({total} in der Sitzung; die Liste ist gekürzt)",
999
1083
  "settings.title": "Übersicht-Seitenleiste",
1000
1084
  "settings.desc": "Schalter für die Abschnitte des Übersicht-Tabs in der rechten Seitenleiste (schreibgeschützt).",
1001
1085
  "settings.memorySlots": "Speicherplätze-Abschnitt",
@@ -1048,6 +1132,9 @@ window.__ModuleLoader__.load({
1048
1132
  "recall.supersededTag": "sostituito dal riassunto",
1049
1133
  "recall.noBody": "(questo frame non contiene il testo completo di questa voce)",
1050
1134
  "section.skillUsage": "Riferimenti alle skill",
1135
+ "section.references": "Riferimenti",
1136
+ "references.tabSkills": "Skill",
1137
+ "references.tabMemory": "Memoria",
1051
1138
  "skillUsage.empty": "Nessuna skill caricata in questa sessione",
1052
1139
  "skillUsage.emptyHint": "Le skill compaiono qui quando il modello ne carica una con il tool `skill` o quando ne avvii una direttamente.",
1053
1140
  "skillUsage.uses": "{count}x",
@@ -1056,6 +1143,16 @@ window.__ModuleLoader__.load({
1056
1143
  "skillUsage.invokedGroup": "Referenziate · {count}",
1057
1144
  "skillUsage.availableGroup": "Disponibili in questa sessione · {count}",
1058
1145
  "skillUsage.availableCapped": "({count} skill nel catalogo; l’elenco è troncato)",
1146
+ "section.searchRefs": "Ricerche web",
1147
+ "references.tabSearch": "Ricerche",
1148
+ "searchRefs.empty": "Nessuna ricerca in questa sessione",
1149
+ "searchRefs.emptyHint": "Le query e le fonti esterne restituite compaiono qui quando il modello chiama advanced_search.",
1150
+ "searchRefs.queryGroup": "Query · {count}",
1151
+ "searchRefs.sourceGroup": "Fonti · {count}",
1152
+ "searchRefs.hits": "{count} restituite",
1153
+ "searchRefs.latestTag": "Questo turno",
1154
+ "searchRefs.noSourcesHint": "(questa build di dsh-free-search non restituisce l’elenco delle fonti; aggiorna per vederlo)",
1155
+ "searchRefs.capped": "({total} nella sessione; l’elenco è troncato)",
1059
1156
  "settings.title": "Barra laterale Riepilogo",
1060
1157
  "settings.desc": "Interruttori delle sezioni della scheda Riepilogo nella barra laterale destra (sola lettura).",
1061
1158
  "settings.memorySlots": "Sezione slot di memoria",
@@ -1108,6 +1205,9 @@ window.__ModuleLoader__.load({
1108
1205
  "recall.supersededTag": "заменено сводкой",
1109
1206
  "recall.noBody": "В этом кадре нет полного текста этой записи",
1110
1207
  "section.skillUsage": "Отсылки к скиллам",
1208
+ "section.references": "Отсылки",
1209
+ "references.tabSkills": "Навыки",
1210
+ "references.tabMemory": "Память",
1111
1211
  "skillUsage.empty": "В этой сессии скиллы не загружались",
1112
1212
  "skillUsage.emptyHint": "Навыки появятся здесь, когда модель загрузит их через инструмент `skill` или вы вызовете навык напрямую.",
1113
1213
  "skillUsage.uses": "{count} раз",
@@ -1116,6 +1216,16 @@ window.__ModuleLoader__.load({
1116
1216
  "skillUsage.invokedGroup": "Использованы · {count}",
1117
1217
  "skillUsage.availableGroup": "Доступны в этой сессии · {count}",
1118
1218
  "skillUsage.availableCapped": "({count} навыков в каталоге; список усечён)",
1219
+ "section.searchRefs": "Поиск в сети",
1220
+ "references.tabSearch": "Поиск",
1221
+ "searchRefs.empty": "В этой сессии ещё не было поиска",
1222
+ "searchRefs.emptyHint": "Запросы и возвращённые внешние источники появятся здесь, как только модель вызовет advanced_search.",
1223
+ "searchRefs.queryGroup": "Запросы · {count}",
1224
+ "searchRefs.sourceGroup": "Источники · {count}",
1225
+ "searchRefs.hits": "возвращено {count}",
1226
+ "searchRefs.latestTag": "Этот ход",
1227
+ "searchRefs.noSourcesHint": "(эта сборка dsh-free-search не возвращает список источников; обновите, чтобы видеть их)",
1228
+ "searchRefs.capped": "(в сессии всего {total}; список усечён)",
1119
1229
  "settings.title": "Боковая панель «Сводка»",
1120
1230
  "settings.desc": "Переключатели разделов вкладки «Сводка» на правой панели (только чтение).",
1121
1231
  "settings.memorySlots": "Раздел слотов памяти",
@@ -1168,6 +1278,9 @@ window.__ModuleLoader__.load({
1168
1278
  "recall.supersededTag": "reemplazado por el resumen",
1169
1279
  "recall.noBody": "(este fotograma no trae el texto completo de este registro)",
1170
1280
  "section.skillUsage": "Referencias de skills",
1281
+ "section.references": "Referencias",
1282
+ "references.tabSkills": "Skills",
1283
+ "references.tabMemory": "Memoria",
1171
1284
  "skillUsage.empty": "Esta sesión no ha cargado ningún skill",
1172
1285
  "skillUsage.emptyHint": "Las skills aparecen aquí cuando el modelo carga una con la herramienta `skill` o cuando invocas una directamente.",
1173
1286
  "skillUsage.uses": "{count}x",
@@ -1176,6 +1289,16 @@ window.__ModuleLoader__.load({
1176
1289
  "skillUsage.invokedGroup": "Referenciadas · {count}",
1177
1290
  "skillUsage.availableGroup": "Disponibles en esta sesión · {count}",
1178
1291
  "skillUsage.availableCapped": "({count} skills en el catálogo; la lista está truncada)",
1292
+ "section.searchRefs": "Búsquedas web",
1293
+ "references.tabSearch": "Búsquedas",
1294
+ "searchRefs.empty": "Sin búsquedas en esta sesión",
1295
+ "searchRefs.emptyHint": "Las consultas y las fuentes externas devueltas aparecen aquí cuando el modelo llama a advanced_search.",
1296
+ "searchRefs.queryGroup": "Consultas · {count}",
1297
+ "searchRefs.sourceGroup": "Fuentes · {count}",
1298
+ "searchRefs.hits": "{count} devueltas",
1299
+ "searchRefs.latestTag": "Este turno",
1300
+ "searchRefs.noSourcesHint": "(esta versión de dsh-free-search no devuelve la lista de fuentes; actualiza para verla)",
1301
+ "searchRefs.capped": "({total} en la sesión; la lista está truncada)",
1179
1302
  "settings.title": "Barra lateral Resumen",
1180
1303
  "settings.desc": "Interruptores de las secciones de la pestaña Resumen en la barra lateral derecha (solo lectura).",
1181
1304
  "settings.memorySlots": "Sección de ranuras de memoria",
@@ -1222,24 +1345,24 @@ window.__ModuleLoader__.load({
1222
1345
  *
1223
1346
  * Colours come from `--dsw-alias-*` tokens only (skin contract).
1224
1347
  */
1225
- const SECTION_STYLE$3 = {
1348
+ const SECTION_STYLE$5 = {
1226
1349
  flexShrink: 0,
1227
1350
  borderBottom: "1px solid var(--dsw-alias-border-secondary)"
1228
1351
  };
1229
- const HEADER_STYLE$4 = {
1352
+ const HEADER_STYLE$6 = {
1230
1353
  padding: "10px 12px 0",
1231
1354
  color: "var(--dsw-alias-label-secondary)",
1232
1355
  fontSize: 12,
1233
1356
  fontWeight: 600
1234
1357
  };
1235
- const LIST_STYLE$3 = {
1358
+ const LIST_STYLE$4 = {
1236
1359
  listStyle: "none",
1237
1360
  margin: 0,
1238
1361
  padding: "6px 0",
1239
1362
  display: "flex",
1240
1363
  flexDirection: "column"
1241
1364
  };
1242
- const ROW_STYLE$4 = {
1365
+ const ROW_STYLE$5 = {
1243
1366
  display: "flex",
1244
1367
  alignItems: "flex-start",
1245
1368
  gap: 8,
@@ -1263,7 +1386,7 @@ window.__ModuleLoader__.load({
1263
1386
  cursor: "pointer"
1264
1387
  };
1265
1388
  /** The codeplan pill: same form as the status pills, tertiary emphasis. */
1266
- const TAG_STYLE$1 = {
1389
+ const TAG_STYLE$2 = {
1267
1390
  flexShrink: 0,
1268
1391
  color: "var(--dsw-alias-label-tertiary)",
1269
1392
  border: "1px solid var(--dsw-alias-label-tertiary)",
@@ -1284,12 +1407,12 @@ window.__ModuleLoader__.load({
1284
1407
  const plan = splitCodeplanPath(path);
1285
1408
  const title = plan === null ? path : `${t("deliverable.codeplanTag")}: ${plan.task} · ${path}`;
1286
1409
  const label = basename(path);
1287
- const content = plan === null ? null : (0, react.createElement)("span", { style: TAG_STYLE$1 }, t("deliverable.codeplanTag"));
1288
- if (open === void 0) return (0, react.createElement)("li", { style: ROW_STYLE$4 }, content, (0, react.createElement)("span", {
1410
+ const content = plan === null ? null : (0, react.createElement)("span", { style: TAG_STYLE$2 }, t("deliverable.codeplanTag"));
1411
+ if (open === void 0) return (0, react.createElement)("li", { style: ROW_STYLE$5 }, content, (0, react.createElement)("span", {
1289
1412
  style: PATH_STYLE,
1290
1413
  title
1291
1414
  }, label));
1292
- return (0, react.createElement)("li", { style: ROW_STYLE$4 }, content, (0, react.createElement)("button", {
1415
+ return (0, react.createElement)("li", { style: ROW_STYLE$5 }, content, (0, react.createElement)("button", {
1293
1416
  style: BUTTON_STYLE,
1294
1417
  title,
1295
1418
  onClick: () => {
@@ -1315,9 +1438,9 @@ window.__ModuleLoader__.load({
1315
1438
  const latestPaths = view.latest?.paths ?? [];
1316
1439
  const history = historyPaths(view.sessionPaths, latestPaths);
1317
1440
  if (latestPaths.length === 0 && view.sessionTotal === 0) return (0, react.createElement)("div", {
1318
- style: SECTION_STYLE$3,
1441
+ style: SECTION_STYLE$5,
1319
1442
  "data-dsh-brief-sidebar": "deliverables-section"
1320
- }, (0, react.createElement)("div", { style: HEADER_STYLE$4 }, t("section.deliverables")), (0, react.createElement)("div", { style: {
1443
+ }, (0, react.createElement)("div", { style: HEADER_STYLE$6 }, t("section.deliverables")), (0, react.createElement)("div", { style: {
1321
1444
  margin: "0 auto",
1322
1445
  padding: 24,
1323
1446
  maxWidth: 460,
@@ -1330,12 +1453,12 @@ window.__ModuleLoader__.load({
1330
1453
  color: "var(--dsw-alias-label-secondary)"
1331
1454
  } }, t("deliverables.empty")), (0, react.createElement)("div", null, t("deliverables.emptyHint"))));
1332
1455
  return (0, react.createElement)("div", {
1333
- style: SECTION_STYLE$3,
1456
+ style: SECTION_STYLE$5,
1334
1457
  "data-dsh-brief-sidebar": "deliverables-section"
1335
- }, (0, react.createElement)("div", { style: HEADER_STYLE$4 }, t("section.deliverables")), latestPaths.length > 0 && (0, react.createElement)(GroupHeader, {
1458
+ }, (0, react.createElement)("div", { style: HEADER_STYLE$6 }, t("section.deliverables")), latestPaths.length > 0 && (0, react.createElement)(GroupHeader, {
1336
1459
  text: expandCount(t("deliverables.currentTurn"), latestPaths.length),
1337
1460
  marker: "deliverables-current-group"
1338
- }), (0, react.createElement)("ul", { style: LIST_STYLE$3 }, latestPaths.map((path) => (0, react.createElement)(PathRow, {
1461
+ }), (0, react.createElement)("ul", { style: LIST_STYLE$4 }, latestPaths.map((path) => (0, react.createElement)(PathRow, {
1339
1462
  key: path,
1340
1463
  path,
1341
1464
  t,
@@ -1350,7 +1473,7 @@ window.__ModuleLoader__.load({
1350
1473
  }), history.length > 0 && view.sessionTotal > view.sessionPaths.length && (0, react.createElement)(GroupCaption, {
1351
1474
  text: expandTotal(t("deliverables.historyCapped"), view.sessionTotal),
1352
1475
  marker: "deliverables-history-capped"
1353
- }), historyOpen && (0, react.createElement)("ul", { style: LIST_STYLE$3 }, history.map((path) => (0, react.createElement)(PathRow, {
1476
+ }), historyOpen && (0, react.createElement)("ul", { style: LIST_STYLE$4 }, history.map((path) => (0, react.createElement)(PathRow, {
1354
1477
  key: path,
1355
1478
  path,
1356
1479
  t,
@@ -1360,7 +1483,7 @@ window.__ModuleLoader__.load({
1360
1483
  //#endregion
1361
1484
  //#region src/client/summary/skill-usage.ts
1362
1485
  /** Whether a value is a non-negative safe integer (the view's counter shape). */
1363
- function isCounter$2(value) {
1486
+ function isCounter$3(value) {
1364
1487
  return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
1365
1488
  }
1366
1489
  /** Narrow one skill tally; `undefined` = malformed, dropped. */
@@ -1368,7 +1491,7 @@ window.__ModuleLoader__.load({
1368
1491
  if (value === null || typeof value !== "object" || Array.isArray(value)) return void 0;
1369
1492
  const raw = value;
1370
1493
  if (typeof raw["name"] !== "string" || raw["name"].length === 0) return void 0;
1371
- if (!isCounter$2(raw["count"]) || !isCounter$2(raw["lastTurn"])) return void 0;
1494
+ if (!isCounter$3(raw["count"]) || !isCounter$3(raw["lastTurn"])) return void 0;
1372
1495
  return {
1373
1496
  name: raw["name"],
1374
1497
  count: raw["count"],
@@ -1379,10 +1502,10 @@ window.__ModuleLoader__.load({
1379
1502
  * The latest-turn bucket, or `null` when it is absent OR drifted. A drifted
1380
1503
  * bucket only costs the "本轮" tag, so it degrades instead of voiding the view.
1381
1504
  */
1382
- function readLatest(value) {
1505
+ function readLatest$1(value) {
1383
1506
  if (value === null || typeof value !== "object" || Array.isArray(value)) return null;
1384
1507
  const raw = value;
1385
- if (!isCounter$2(raw["turn"]) || !Array.isArray(raw["names"])) return null;
1508
+ if (!isCounter$3(raw["turn"]) || !Array.isArray(raw["names"])) return null;
1386
1509
  return {
1387
1510
  turn: raw["turn"],
1388
1511
  names: raw["names"].filter((n) => typeof n === "string" && n.length > 0)
@@ -1410,16 +1533,16 @@ window.__ModuleLoader__.load({
1410
1533
  if (value === null || typeof value !== "object" || Array.isArray(value)) return void 0;
1411
1534
  const raw = value;
1412
1535
  if (!Array.isArray(raw["session"])) return void 0;
1413
- if (!isCounter$2(raw["sessionTotal"]) || !isCounter$2(raw["callTotal"])) return void 0;
1536
+ if (!isCounter$3(raw["sessionTotal"]) || !isCounter$3(raw["callTotal"])) return void 0;
1414
1537
  const session = [];
1415
1538
  for (const entry of raw["session"]) {
1416
1539
  const narrowed = readEntry(entry);
1417
1540
  if (narrowed !== void 0) session.push(narrowed);
1418
1541
  }
1419
1542
  const available = readAvailable(raw["available"]);
1420
- const availableCount = available.length === 0 ? 0 : isCounter$2(raw["availableCount"]) ? raw["availableCount"] : available.length;
1543
+ const availableCount = available.length === 0 ? 0 : isCounter$3(raw["availableCount"]) ? raw["availableCount"] : available.length;
1421
1544
  return {
1422
- latest: readLatest(raw["latest"]),
1545
+ latest: readLatest$1(raw["latest"]),
1423
1546
  session,
1424
1547
  sessionTotal: raw["sessionTotal"],
1425
1548
  callTotal: raw["callTotal"],
@@ -1456,216 +1579,284 @@ window.__ModuleLoader__.load({
1456
1579
  return template.replace("{count}", String(count)).replace("{total}", String(total));
1457
1580
  }
1458
1581
  //#endregion
1459
- //#region src/client/summary/SkillUsageSection.tsx
1460
- /**
1461
- * The skill-usage section: the `dshSummarySkillUsage` projection drawn as one
1462
- * part of the summary tab.
1463
- *
1464
- * Where `MemoryRecallSection` answers "what memories did the model see", this
1465
- * answers "which instruction packs entered the context, and which ones were on
1466
- * the menu". Two groups, deliberately not merged:
1467
- *
1468
- * - **已引用** — the skill's instructions reached the conversation, either
1469
- * because the model called the `skill` tool or because the user invoked it
1470
- * directly (the host then injects `<skill_content>` and tells the model not
1471
- * to call the tool — reading only the tool would miss over half of those
1472
- * sessions, measured on 250 real ones).
1473
- * - **本会话可用** — the session's current catalog. Available is NOT used: the
1474
- * model may never touch any of these, so it is collapsed behind a counted
1475
- * toggle and never mixed into the reference rows.
1476
- *
1477
- * Like the deliverables section this is an OPTIONAL capability on both sides:
1478
- * no projection registry (or a host half that never registered the unit) means
1479
- * the client reads capability absence and the section hides. No settings switch
1480
- * gates it either — the list is a read-only fact about the session, not a
1481
- * preference.
1482
- *
1483
- * Two contracts this row honours, both learned on the recall section:
1484
- * - **no native `title` attribute** — it paints an unskinable tooltip that
1485
- * overlaps the plugin's own bubble; nothing here needs one;
1486
- * - **colours from `--dsw-alias-*` tokens only** (skin contract).
1487
- */
1488
- const SECTION_STYLE$2 = {
1489
- flexShrink: 0,
1490
- borderBottom: "1px solid var(--dsw-alias-border-secondary)"
1491
- };
1492
- const HEADER_STYLE$3 = {
1493
- padding: "10px 12px 0",
1494
- color: "var(--dsw-alias-label-secondary)",
1495
- fontSize: 12,
1496
- fontWeight: 600
1497
- };
1498
- const LIST_STYLE$2 = {
1499
- listStyle: "none",
1500
- margin: 0,
1501
- padding: "6px 0",
1502
- display: "flex",
1503
- flexDirection: "column"
1504
- };
1505
- const ROW_STYLE$3 = {
1506
- display: "flex",
1507
- alignItems: "baseline",
1508
- gap: 8,
1509
- padding: "6px 12px"
1510
- };
1511
- /** The skill name: primary-ish but never the loudest thing in the row. */
1512
- const NAME_STYLE$1 = {
1513
- minWidth: 0,
1514
- overflowWrap: "anywhere",
1515
- color: "var(--dsw-alias-label-secondary)"
1516
- };
1517
- /** The per-skill load tally, tertiary so the name stays the reading target. */
1518
- const COUNT_STYLE = {
1519
- flexShrink: 0,
1520
- color: "var(--dsw-alias-label-tertiary)",
1521
- fontSize: 11
1522
- };
1523
- /** The "本轮" pill: same form as the deliverables codeplan tag. */
1524
- const TAG_STYLE = {
1525
- flexShrink: 0,
1526
- color: "var(--dsw-alias-label-tertiary)",
1527
- border: "1px solid var(--dsw-alias-label-tertiary)",
1528
- borderRadius: 999,
1529
- padding: "0 6px",
1530
- fontSize: 11,
1531
- lineHeight: "16px",
1532
- whiteSpace: "nowrap"
1533
- };
1534
- const FOOTER_STYLE$2 = {
1535
- padding: "0 12px 10px",
1536
- color: "var(--dsw-alias-label-tertiary)",
1537
- fontSize: 11
1538
- };
1539
- const EMPTY_STYLE$2 = {
1540
- margin: "0 auto",
1541
- padding: 24,
1542
- maxWidth: 460,
1543
- textAlign: "center",
1544
- color: "var(--dsw-alias-label-tertiary)",
1545
- lineHeight: 1.7,
1546
- fontSize: 12
1547
- };
1582
+ //#region src/projection/search-ref-fold.ts
1548
1583
  /**
1549
- * The skill-usage section. The subscription is unconditional (a plain listener,
1550
- * not IO); the view decides whether anything is drawn.
1551
- *
1552
- * @param props - translation, client context, session scope.
1584
+ * The dedupe key. Host case and a trailing slash are not meaningful for
1585
+ * identity; path case IS, so it is left alone. Exported because the client
1586
+ * matches a row against the latest-turn bucket with the same algorithm.
1553
1587
  */
1554
- function SkillUsageSection(props) {
1555
- const { t, ctx, scope } = props;
1556
- const [availableOpen, setAvailableOpen] = (0, react.useState)(false);
1557
- const view = readSkillUsage(useProjectionValue(ctx, scope?.sessionId, SKILL_USAGE_KEY));
1558
- if (view === void 0) return null;
1559
- const referenced = view.session.map((entry) => entry.name);
1560
- const rest = availableExcept(view.available, referenced);
1561
- const latestNames = view.latest?.names ?? [];
1562
- return (0, react.createElement)("div", {
1563
- style: SECTION_STYLE$2,
1564
- "data-dsh-brief-sidebar": "skill-usage-section"
1565
- }, (0, react.createElement)("div", { style: HEADER_STYLE$3 }, t("section.skillUsage")), view.session.length === 0 && (0, react.createElement)("div", { style: EMPTY_STYLE$2 }, (0, react.createElement)("div", { style: {
1566
- marginBottom: 6,
1567
- color: "var(--dsw-alias-label-secondary)"
1568
- } }, t("skillUsage.empty")), (0, react.createElement)("div", null, t("skillUsage.emptyHint"))), view.session.length > 0 && (0, react.createElement)(GroupHeader, {
1569
- text: expandCount(t("skillUsage.invokedGroup"), view.session.length),
1570
- marker: "skill-usage-invoked-group"
1571
- }), (0, react.createElement)("ul", { style: LIST_STYLE$2 }, view.session.map((entry) => (0, react.createElement)("li", {
1572
- key: entry.name,
1573
- style: ROW_STYLE$3,
1574
- "data-dsh-brief-sidebar": "skill-usage-row"
1575
- }, (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 }, t("skillUsage.latestTag"))))), rest.length > 0 && (0, react.createElement)(FoldToggle, {
1576
- label: expandCount(t("skillUsage.availableGroup"), rest.length),
1577
- open: availableOpen,
1578
- marker: "skill-usage-available-toggle",
1579
- onToggle: () => {
1580
- setAvailableOpen((current) => !current);
1581
- }
1582
- }), rest.length > 0 && view.availableCount > view.available.length && (0, react.createElement)(GroupCaption, {
1583
- text: expandCount(t("skillUsage.availableCapped"), view.availableCount),
1584
- marker: "skill-usage-available-capped"
1585
- }), availableOpen && (0, react.createElement)("ul", { style: LIST_STYLE$2 }, rest.map((name) => (0, react.createElement)("li", {
1586
- key: name,
1587
- style: ROW_STYLE$3,
1588
- "data-dsh-brief-sidebar": "skill-usage-available-row"
1589
- }, (0, react.createElement)("span", { style: NAME_STYLE$1 }, name)))), view.session.length > 0 && (0, react.createElement)("div", { style: FOOTER_STYLE$2 }, expandPair(t("skillUsage.count"), view.callTotal, view.sessionTotal)));
1588
+ function urlKey(url) {
1589
+ const trimmed = url.trim();
1590
+ return (trimmed.endsWith("/") ? trimmed.slice(0, -1) : trimmed).replace(/^[a-z][a-z0-9+.-]*:\/\/[^/]+/i, (match) => match.toLowerCase());
1590
1591
  }
1591
1592
  //#endregion
1592
- //#region src/client/summary/memory-slots.ts
1593
+ //#region src/client/summary/search-refs.ts
1593
1594
  /**
1594
- * The `memorySlots` projection — reading it.
1595
+ * The `dshSummarySearchRefs` projection — reading it.
1595
1596
  *
1596
- * The projection unit is registered by `dsh-prime-memory` (key `memorySlots`,
1597
- * wire view `{rev, count, openCount, slots[]}`); this plugin does NOT depend
1598
- * on it — when the unit is not registered the client reads `undefined` from
1599
- * `faceOf` and the section hides. This module is the read side of that
1600
- * contract and is deliberately PURE — no React, no context, no IO — so the
1601
- * narrowing can be tested directly.
1597
+ * The unit is registered by THIS plugin's host half (key `dshSummarySearchRefs`):
1598
+ * the queries this session ran through `advanced_search`, and the sources those
1599
+ * searches brought back. It answers "what did the session pull in from outside"
1600
+ * — where the skill tab says which instruction packs were loaded and the memory
1601
+ * tab says which of the user's own knowledge was injected.
1602
1602
  *
1603
- * Forward compatibility: the shipped view carries `{id, title, kind, status,
1604
- * priority}` only. A future prime-memory release may add `body`/`refs` to the
1605
- * wire view (its projection plan keeps that door open); those fields are
1606
- * narrowed opportunistically — present and well-formed → kept, absent or
1607
- * malformed → dropped — so the section renders them without a brief-sidebar
1608
- * change, and a malformed extra field can never blank the section.
1603
+ * Reading discipline, same as every other brief section:
1604
+ * - `undefined` = the capability is ABSENT (no projection registry, or a frame
1605
+ * drifted past repair) → the tab hides;
1606
+ * - empty lists = the honest empty state (the session has not searched yet);
1607
+ * - a malformed row is dropped, never thrown.
1608
+ *
1609
+ * The module is deliberately PURE — no React, no context, no IO.
1609
1610
  */
1610
- /** The projection key `dsh-prime-memory` registers (its canonical constant). */
1611
- const MEMORY_SLOTS_KEY = "memorySlots";
1611
+ /** Whether a value is a non-negative safe integer (the view's counters). */
1612
+ function isCounter$2(value) {
1613
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
1614
+ }
1615
+ /** Narrow one source row; `undefined` = malformed, dropped. */
1616
+ function readItem$1(value) {
1617
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return void 0;
1618
+ const raw = value;
1619
+ if (typeof raw["url"] !== "string" || raw["url"].length === 0) return void 0;
1620
+ if (typeof raw["host"] !== "string") return void 0;
1621
+ if (typeof raw["query"] !== "string") return void 0;
1622
+ if (!isCounter$2(raw["count"]) || !isCounter$2(raw["lastTurn"])) return void 0;
1623
+ const item = {
1624
+ url: raw["url"],
1625
+ host: raw["host"],
1626
+ query: raw["query"],
1627
+ count: raw["count"],
1628
+ lastTurn: raw["lastTurn"]
1629
+ };
1630
+ if (typeof raw["title"] === "string" && raw["title"].length > 0) item.title = raw["title"];
1631
+ if (typeof raw["snippet"] === "string" && raw["snippet"].length > 0) item.snippet = raw["snippet"];
1632
+ return item;
1633
+ }
1634
+ /** Narrow one query row; `undefined` = malformed, dropped. */
1635
+ function readQuery(value) {
1636
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return void 0;
1637
+ const raw = value;
1638
+ if (typeof raw["query"] !== "string" || raw["query"].length === 0) return void 0;
1639
+ if (!isCounter$2(raw["turn"]) || !isCounter$2(raw["hits"])) return void 0;
1640
+ return {
1641
+ query: raw["query"],
1642
+ turn: raw["turn"],
1643
+ hits: raw["hits"]
1644
+ };
1645
+ }
1646
+ /** The latest-turn key set, or `null` when absent OR drifted (only a tag rides it). */
1647
+ function readLatest(value) {
1648
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return null;
1649
+ const raw = value;
1650
+ if (!isCounter$2(raw["turn"]) || !Array.isArray(raw["urls"])) return null;
1651
+ return {
1652
+ turn: raw["turn"],
1653
+ urls: raw["urls"].filter((u) => typeof u === "string" && u.length > 0)
1654
+ };
1655
+ }
1612
1656
  /**
1613
- * Narrow a raw projection value into a memory-slots view.
1614
- *
1615
- * Two outcomes are kept apart, mirroring the deliverables read side:
1616
- * - `undefined` — the capability is ABSENT (prime-memory is not in the
1617
- * composition, or no frame has carried the key yet), or the frame is
1618
- * malformed beyond repair (a shape drift must not blank the whole tab);
1619
- * - a view — the projection is live. A view with `slots: []` IS the empty
1620
- * state: prime-memory publishes as soon as the unit registers, so "present
1621
- * but empty" is a real value, not a sentinel.
1622
- *
1623
- * Malformed slot entries are dropped rather than thrown on (trusted-Host
1624
- * value, defensive display), like `readTodos`.
1657
+ * Narrow a raw projection value into a search-reference view.
1625
1658
  *
1626
- * @param value - the raw `unknown` snapshot of the `memorySlots` face.
1659
+ * @param value - the raw `unknown` snapshot of the `dshSummarySearchRefs` face.
1627
1660
  * @returns the validated view, or `undefined` when the capability is absent.
1628
1661
  */
1629
- function readSlots(value) {
1662
+ function readSearchRefs(value) {
1630
1663
  if (value === null || typeof value !== "object" || Array.isArray(value)) return void 0;
1631
1664
  const raw = value;
1632
- if (!isCounter$1(raw["rev"]) || !isCounter$1(raw["count"]) || !isCounter$1(raw["openCount"])) return void 0;
1633
- if (!Array.isArray(raw["slots"])) return void 0;
1634
- const slots = [];
1635
- for (const entry of raw["slots"]) {
1636
- const slot = readSlot(entry);
1637
- if (slot !== void 0) slots.push(slot);
1665
+ if (!Array.isArray(raw["queries"]) || !Array.isArray(raw["sources"])) return void 0;
1666
+ if (!isCounter$2(raw["queryTotal"]) || !isCounter$2(raw["sourceTotal"])) return void 0;
1667
+ const queries = [];
1668
+ for (const entry of raw["queries"]) {
1669
+ const narrowed = readQuery(entry);
1670
+ if (narrowed !== void 0) queries.push(narrowed);
1671
+ }
1672
+ const sources = [];
1673
+ for (const entry of raw["sources"]) {
1674
+ const narrowed = readItem$1(entry);
1675
+ if (narrowed !== void 0) sources.push(narrowed);
1638
1676
  }
1639
1677
  return {
1640
- rev: raw["rev"],
1641
- count: raw["count"],
1642
- openCount: raw["openCount"],
1643
- slots
1678
+ latest: readLatest(raw["latest"]),
1679
+ queries,
1680
+ sources,
1681
+ queryTotal: raw["queryTotal"],
1682
+ sourceTotal: raw["sourceTotal"]
1644
1683
  };
1645
1684
  }
1685
+ /** Whether one source row was surfaced by the latest turn's searches. */
1686
+ function isLatestSource(view, url) {
1687
+ return view.latest?.urls.includes(urlKey(url)) === true;
1688
+ }
1689
+ /**
1690
+ * The href to render for a source URL, or `undefined` when it must not be a link.
1691
+ *
1692
+ * Search results are untrusted external data: a `javascript:` or `data:` URL in
1693
+ * a provider response would otherwise become a clickable footgun in the user's
1694
+ * own sidebar. Only web schemes get an anchor; everything else renders as text.
1695
+ */
1696
+ function safeHref(url) {
1697
+ try {
1698
+ const parsed = new URL(url);
1699
+ return parsed.protocol === "https:" || parsed.protocol === "http:" ? parsed.href : void 0;
1700
+ } catch {
1701
+ return;
1702
+ }
1703
+ }
1704
+ //#endregion
1705
+ //#region src/client/summary/memory-recall.ts
1706
+ /**
1707
+ * The `memoryRecall` projection — reading it.
1708
+ *
1709
+ * The projection unit is registered by `dsh-prime-memory` (key `memoryRecall`,
1710
+ * wire view `{rev, sessionId, count, items[]}`): the memories this session has
1711
+ * actually injected. Unlike
1712
+ * `memorySlots` (pinned, resident conventions written by hand), this set is
1713
+ * NOT resident — it is the at-a-glance answer to "what did the model see".
1714
+ * It survives compaction on purpose: `compact` only marks the entries whose
1715
+ * text left the context as `superseded` (and `/clear` empties the set), so a
1716
+ * retired row keeps its strike-through instead of vanishing.
1717
+ *
1718
+ * This plugin does NOT depend on prime-memory: with the unit unregistered the
1719
+ * client reads `undefined` from the face and the section hides. The module is
1720
+ * deliberately PURE — no React, no context, no IO — so the narrowing is
1721
+ * testable directly, exactly like `memory-slots.ts`.
1722
+ */
1723
+ /** The projection key `dsh-prime-memory` registers (its canonical constant). */
1724
+ const MEMORY_RECALL_KEY = "memoryRecall";
1646
1725
  /** Whether a value is a non-negative safe integer (the view's counter shape). */
1647
1726
  function isCounter$1(value) {
1648
1727
  return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
1649
1728
  }
1650
- /** Narrow one slot entry; `undefined` = malformed, dropped. */
1651
- function readSlot(value) {
1729
+ /** Narrow one item entry; `undefined` = malformed, dropped. */
1730
+ function readItem(value) {
1652
1731
  if (value === null || typeof value !== "object" || Array.isArray(value)) return void 0;
1653
1732
  const raw = value;
1654
1733
  if (typeof raw["id"] !== "string" || raw["id"].length === 0) return void 0;
1655
- if (typeof raw["title"] !== "string") return void 0;
1656
- if (typeof raw["kind"] !== "string" || typeof raw["status"] !== "string") return void 0;
1734
+ if (typeof raw["type"] !== "string" || typeof raw["title"] !== "string") return void 0;
1657
1735
  if (typeof raw["priority"] !== "number") return void 0;
1658
- const slot = {
1736
+ const item = {
1659
1737
  id: raw["id"],
1738
+ type: raw["type"],
1660
1739
  title: raw["title"],
1661
- kind: raw["kind"],
1662
- status: raw["status"],
1663
1740
  priority: raw["priority"]
1664
1741
  };
1665
- if (typeof raw["body"] === "string" && raw["body"].length > 0) slot.body = raw["body"];
1666
- if (Array.isArray(raw["refs"])) {
1667
- const refs = raw["refs"].filter((ref) => typeof ref === "string" && ref.length > 0);
1668
- if (refs.length > 0) slot.refs = refs;
1742
+ if (Array.isArray(raw["scenario"])) {
1743
+ const phrases = raw["scenario"].filter((s) => typeof s === "string" && s.length > 0);
1744
+ if (phrases.length > 0) item.scenario = phrases;
1745
+ }
1746
+ const rating = raw["rating"];
1747
+ if (typeof rating === "number" && Number.isInteger(rating) && rating >= -1 && rating <= 5) item.rating = rating;
1748
+ if (typeof raw["body"] === "string") item.body = raw["body"];
1749
+ if (raw["retired"] === true) item.retired = true;
1750
+ if (raw["superseded"] === true) item.superseded = true;
1751
+ return item;
1752
+ }
1753
+ /**
1754
+ * Narrow a raw projection value into a recall view.
1755
+ *
1756
+ * `undefined` means the capability is ABSENT (prime-memory not in the
1757
+ * composition, or no frame carried the key) or the frame drifted beyond repair;
1758
+ * a view with `items: []` IS the honest empty state (prime-memory publishes as
1759
+ * soon as the unit registers). Malformed items are dropped, never thrown on.
1760
+ *
1761
+ * @param value - the raw `unknown` snapshot of the `memoryRecall` face.
1762
+ * @returns the validated view, or `undefined` when the capability is absent.
1763
+ */
1764
+ function readRecall(value) {
1765
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return void 0;
1766
+ const raw = value;
1767
+ if (!isCounter$1(raw["rev"]) || !isCounter$1(raw["count"])) return void 0;
1768
+ if (typeof raw["sessionId"] !== "string") return void 0;
1769
+ if (!Array.isArray(raw["items"])) return void 0;
1770
+ const items = [];
1771
+ for (const entry of raw["items"]) {
1772
+ const item = readItem(entry);
1773
+ if (item !== void 0) items.push(item);
1774
+ }
1775
+ return {
1776
+ rev: raw["rev"],
1777
+ sessionId: raw["sessionId"],
1778
+ count: raw["count"],
1779
+ items
1780
+ };
1781
+ }
1782
+ //#endregion
1783
+ //#region src/client/summary/memory-slots.ts
1784
+ /**
1785
+ * The `memorySlots` projection — reading it.
1786
+ *
1787
+ * The projection unit is registered by `dsh-prime-memory` (key `memorySlots`,
1788
+ * wire view `{rev, count, openCount, slots[]}`); this plugin does NOT depend
1789
+ * on it — when the unit is not registered the client reads `undefined` from
1790
+ * `faceOf` and the section hides. This module is the read side of that
1791
+ * contract and is deliberately PURE — no React, no context, no IO — so the
1792
+ * narrowing can be tested directly.
1793
+ *
1794
+ * Forward compatibility: the shipped view carries `{id, title, kind, status,
1795
+ * priority}` only. A future prime-memory release may add `body`/`refs` to the
1796
+ * wire view (its projection plan keeps that door open); those fields are
1797
+ * narrowed opportunistically — present and well-formed → kept, absent or
1798
+ * malformed → dropped — so the section renders them without a brief-sidebar
1799
+ * change, and a malformed extra field can never blank the section.
1800
+ */
1801
+ /** The projection key `dsh-prime-memory` registers (its canonical constant). */
1802
+ const MEMORY_SLOTS_KEY = "memorySlots";
1803
+ /**
1804
+ * Narrow a raw projection value into a memory-slots view.
1805
+ *
1806
+ * Two outcomes are kept apart, mirroring the deliverables read side:
1807
+ * - `undefined` — the capability is ABSENT (prime-memory is not in the
1808
+ * composition, or no frame has carried the key yet), or the frame is
1809
+ * malformed beyond repair (a shape drift must not blank the whole tab);
1810
+ * - a view — the projection is live. A view with `slots: []` IS the empty
1811
+ * state: prime-memory publishes as soon as the unit registers, so "present
1812
+ * but empty" is a real value, not a sentinel.
1813
+ *
1814
+ * Malformed slot entries are dropped rather than thrown on (trusted-Host
1815
+ * value, defensive display), like `readTodos`.
1816
+ *
1817
+ * @param value - the raw `unknown` snapshot of the `memorySlots` face.
1818
+ * @returns the validated view, or `undefined` when the capability is absent.
1819
+ */
1820
+ function readSlots(value) {
1821
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return void 0;
1822
+ const raw = value;
1823
+ if (!isCounter(raw["rev"]) || !isCounter(raw["count"]) || !isCounter(raw["openCount"])) return void 0;
1824
+ if (!Array.isArray(raw["slots"])) return void 0;
1825
+ const slots = [];
1826
+ for (const entry of raw["slots"]) {
1827
+ const slot = readSlot(entry);
1828
+ if (slot !== void 0) slots.push(slot);
1829
+ }
1830
+ return {
1831
+ rev: raw["rev"],
1832
+ count: raw["count"],
1833
+ openCount: raw["openCount"],
1834
+ slots
1835
+ };
1836
+ }
1837
+ /** Whether a value is a non-negative safe integer (the view's counter shape). */
1838
+ function isCounter(value) {
1839
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
1840
+ }
1841
+ /** Narrow one slot entry; `undefined` = malformed, dropped. */
1842
+ function readSlot(value) {
1843
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return void 0;
1844
+ const raw = value;
1845
+ if (typeof raw["id"] !== "string" || raw["id"].length === 0) return void 0;
1846
+ if (typeof raw["title"] !== "string") return void 0;
1847
+ if (typeof raw["kind"] !== "string" || typeof raw["status"] !== "string") return void 0;
1848
+ if (typeof raw["priority"] !== "number") return void 0;
1849
+ const slot = {
1850
+ id: raw["id"],
1851
+ title: raw["title"],
1852
+ kind: raw["kind"],
1853
+ status: raw["status"],
1854
+ priority: raw["priority"]
1855
+ };
1856
+ if (typeof raw["body"] === "string" && raw["body"].length > 0) slot.body = raw["body"];
1857
+ if (Array.isArray(raw["refs"])) {
1858
+ const refs = raw["refs"].filter((ref) => typeof ref === "string" && ref.length > 0);
1859
+ if (refs.length > 0) slot.refs = refs;
1669
1860
  }
1670
1861
  if (Array.isArray(raw["refViews"])) {
1671
1862
  const views = [];
@@ -1755,63 +1946,72 @@ window.__ModuleLoader__.load({
1755
1946
  return resolve((0, react.useSyncExternalStore)(subscribe, getSnapshot, getSnapshot));
1756
1947
  }
1757
1948
  //#endregion
1758
- //#region src/client/summary/MemorySlotsSection.tsx
1949
+ //#region src/client/summary/SkillUsageSection.tsx
1759
1950
  /**
1760
- * The memory-slots section: the `memorySlots` projection drawn as the third
1761
- * part of the summary tab (fixed last: 进展 → 产物 → 记忆).
1951
+ * The skill-usage section: the `dshSummarySkillUsage` projection drawn as one
1952
+ * part of the summary tab.
1762
1953
  *
1763
- * TWO gates, both fail-open to "hidden", so a host without dsh-prime-memory
1764
- * paints nothing here:
1954
+ * Where `MemoryRecallSection` answers "what memories did the model see", this
1955
+ * answers "which instruction packs entered the context, and which ones were on
1956
+ * the menu". Two groups, deliberately not merged:
1765
1957
  *
1766
- * 1. **The switch.** This plugin's entry config (`configForms` → entry
1767
- * `dsh-brief-sidebar`, field `showMemorySlots`, default OFF — the pinned
1768
- * list moved out of the summary pane's way in favour of the session-recall
1769
- * section). When the config face cannot answer (service absent, scope not
1770
- * ready, field unset) the default applies — see `scope-face.ts`.
1771
- * 2. **The capability.** The `memorySlots` projection must exist for the
1772
- * session; `undefined` hides the section entirely, exactly like the
1773
- * optional deliverables section.
1958
+ * - **已引用** — the skill's instructions reached the conversation, either
1959
+ * because the model called the `skill` tool or because the user invoked it
1960
+ * directly (the host then injects `<skill_content>` and tells the model not
1961
+ * to call the tool — reading only the tool would miss over half of those
1962
+ * sessions, measured on 250 real ones).
1963
+ * - **本会话可用** — the session's current catalog. Available is NOT used: the
1964
+ * model may never touch any of these, so it is collapsed behind a counted
1965
+ * toggle and never mixed into the reference rows.
1774
1966
  *
1775
- * Colours come from `--dsw-alias-*` tokens only (skin contract, enforced by
1776
- * `tests/purity.spec.ts`).
1967
+ * Like the deliverables section this is an OPTIONAL capability on both sides:
1968
+ * no projection registry (or a host half that never registered the unit) means
1969
+ * the client reads capability absence and the section hides. No settings switch
1970
+ * gates it either — the list is a read-only fact about the session, not a
1971
+ * preference.
1972
+ *
1973
+ * Two contracts this row honours, both learned on the recall section:
1974
+ * - **no native `title` attribute** — it paints an unskinable tooltip that
1975
+ * overlaps the plugin's own bubble; nothing here needs one;
1976
+ * - **colours from `--dsw-alias-*` tokens only** (skin contract).
1777
1977
  */
1778
- const SECTION_STYLE$1 = {
1978
+ const SECTION_STYLE$4 = {
1779
1979
  flexShrink: 0,
1780
1980
  borderBottom: "1px solid var(--dsw-alias-border-secondary)"
1781
1981
  };
1782
- const HEADER_STYLE$2 = {
1982
+ const HEADER_STYLE$5 = {
1783
1983
  padding: "10px 12px 0",
1784
1984
  color: "var(--dsw-alias-label-secondary)",
1785
1985
  fontSize: 12,
1786
1986
  fontWeight: 600
1787
1987
  };
1788
- const LIST_STYLE$1 = {
1988
+ const LIST_STYLE$3 = {
1789
1989
  listStyle: "none",
1790
1990
  margin: 0,
1791
1991
  padding: "6px 0",
1792
1992
  display: "flex",
1793
1993
  flexDirection: "column"
1794
1994
  };
1795
- const ROW_STYLE$2 = {
1995
+ const ROW_STYLE$4 = {
1796
1996
  display: "flex",
1797
- alignItems: "flex-start",
1997
+ alignItems: "baseline",
1798
1998
  gap: 8,
1799
1999
  padding: "6px 12px"
1800
2000
  };
1801
- const TITLE_STYLE$1 = {
2001
+ /** The skill name: primary-ish but never the loudest thing in the row. */
2002
+ const NAME_STYLE$1 = {
1802
2003
  minWidth: 0,
1803
2004
  overflowWrap: "anywhere",
1804
2005
  color: "var(--dsw-alias-label-secondary)"
1805
2006
  };
1806
- const META_STYLE$1 = {
2007
+ /** The per-skill load tally, tertiary so the name stays the reading target. */
2008
+ const COUNT_STYLE = {
1807
2009
  flexShrink: 0,
1808
2010
  color: "var(--dsw-alias-label-tertiary)",
1809
- fontSize: 11,
1810
- lineHeight: "18px",
1811
- whiteSpace: "nowrap"
2011
+ fontSize: 11
1812
2012
  };
1813
- /** The kind pill: same form as the codeplan pill, tertiary emphasis. */
1814
- const KIND_STYLE = {
2013
+ /** The "本轮" pill: same form as the deliverables codeplan tag. */
2014
+ const TAG_STYLE$1 = {
1815
2015
  flexShrink: 0,
1816
2016
  color: "var(--dsw-alias-label-tertiary)",
1817
2017
  border: "1px solid var(--dsw-alias-label-tertiary)",
@@ -1821,40 +2021,12 @@ window.__ModuleLoader__.load({
1821
2021
  lineHeight: "16px",
1822
2022
  whiteSpace: "nowrap"
1823
2023
  };
1824
- /** The status pill: filled tint for an open slot, plain text otherwise. */
1825
- const STATUS_OPEN_STYLE = {
1826
- flexShrink: 0,
1827
- color: "var(--dsw-alias-label-primary)",
1828
- border: "1px solid var(--dsw-alias-label-secondary)",
1829
- borderRadius: 999,
1830
- padding: "0 6px",
1831
- fontSize: 11,
1832
- lineHeight: "16px",
1833
- whiteSpace: "nowrap"
1834
- };
1835
- const STATUS_SETTLED_STYLE = {
1836
- ...STATUS_OPEN_STYLE,
1837
- color: "var(--dsw-alias-label-tertiary)",
1838
- border: "1px solid var(--dsw-alias-label-tertiary)"
1839
- };
1840
- const BODY_STYLE$2 = {
1841
- margin: 0,
1842
- minWidth: 0,
1843
- overflowWrap: "anywhere",
2024
+ const FOOTER_STYLE$3 = {
2025
+ padding: "0 12px 10px",
1844
2026
  color: "var(--dsw-alias-label-tertiary)",
1845
- fontSize: 12,
1846
- lineHeight: "18px"
1847
- };
1848
- const REFS_STYLE = {
1849
- ...BODY_STYLE$2,
1850
2027
  fontSize: 11
1851
2028
  };
1852
- const TOTAL_STYLE = {
1853
- padding: "6px 12px 10px",
1854
- color: "var(--dsw-alias-label-tertiary)",
1855
- fontSize: 12
1856
- };
1857
- const EMPTY_STYLE$1 = {
2029
+ const EMPTY_STYLE$3 = {
1858
2030
  margin: "0 auto",
1859
2031
  padding: 24,
1860
2032
  maxWidth: 460,
@@ -1863,153 +2035,230 @@ window.__ModuleLoader__.load({
1863
2035
  lineHeight: 1.7,
1864
2036
  fontSize: 12
1865
2037
  };
1866
- /** Locale key per known slot status; unknown statuses fall back to the raw wire value. */
1867
- const STATUS_KEY = {
1868
- open: "slots.status.open",
1869
- done: "slots.status.done",
1870
- dropped: "slots.status.dropped",
1871
- expired: "slots.status.expired"
1872
- };
1873
2038
  /**
1874
- * One slot row: kind pill, title (full body rides the tooltip), status pill,
1875
- * priority; body line underneath when the wire view carries it; refs as
1876
- * resolved 名称简述 (record_id → title) with the raw refs on the tooltip, or
1877
- * raw refs when the host has not resolved them.
1878
- */
1879
- function SlotRow(props) {
1880
- const { slot, t } = props;
1881
- const statusLabel = STATUS_KEY[slot.status] === void 0 ? slot.status : t(STATUS_KEY[slot.status]);
1882
- const title = slot.body === void 0 ? slot.title : `${slot.title} — ${slot.body}`;
1883
- const refsLine = slot.refViews !== void 0 ? slot.refViews.map((view) => view.title).join(" · ") : slot.refs !== void 0 ? slot.refs.join(" · ") : void 0;
1884
- return (0, react.createElement)("li", { style: ROW_STYLE$2 }, (0, react.createElement)("span", { style: KIND_STYLE }, slot.kind), (0, react.createElement)("div", { style: {
1885
- minWidth: 0,
1886
- flex: "1 1 auto",
1887
- display: "flex",
1888
- flexDirection: "column",
1889
- gap: 2
1890
- } }, (0, react.createElement)("span", {
1891
- style: TITLE_STYLE$1,
1892
- title
1893
- }, slot.title), slot.body !== void 0 && (0, react.createElement)("p", { style: BODY_STYLE$2 }, truncateBody(slot.body)), refsLine !== void 0 && (0, react.createElement)("p", {
1894
- style: REFS_STYLE,
1895
- title: slot.refs !== void 0 ? slot.refs.join("\n") : void 0
1896
- }, refsLine)), (0, react.createElement)("span", { style: META_STYLE$1 }, `P${slot.priority}`), (0, react.createElement)("span", { style: slot.status === "open" ? STATUS_OPEN_STYLE : STATUS_SETTLED_STYLE }, statusLabel));
1897
- }
1898
- /**
1899
- * Subscribe to this plugin's entry config and resolve the slots switch.
1900
- * Plumbing lives in `use-config-flag`; the default (off) lives in the resolver.
2039
+ * The skill-usage section. The subscription is unconditional (a plain listener,
2040
+ * not IO); the view decides whether anything is drawn.
1901
2041
  *
1902
- * @param ctx - the client root context handed to the tab body.
1903
- * @returns whether the memory-slots section may render at all.
1904
- */
1905
- function useShowMemorySlots(ctx) {
1906
- return useConfigFlag(ctx, resolveShowMemorySlots);
1907
- }
1908
- /**
1909
- * The memory-slots section. Both hooks run unconditionally (a conditional
1910
- * hook order would break the moment the switch flips); the switch only
1911
- * decides whether the resolved view paints.
1912
2042
  * @param props - translation, client context, session scope.
1913
2043
  */
1914
- function MemorySlotsSection(props) {
1915
- const { t, ctx, scope } = props;
1916
- const show = useShowMemorySlots(ctx);
1917
- const view = readSlots(useProjectionValue(ctx, scope?.sessionId, MEMORY_SLOTS_KEY));
1918
- if (show === false) return null;
2044
+ function SkillUsageSection(props) {
2045
+ const { t, ctx, scope, hideHeader = false } = props;
2046
+ const [availableOpen, setAvailableOpen] = (0, react.useState)(false);
2047
+ const view = readSkillUsage(useProjectionValue(ctx, scope?.sessionId, SKILL_USAGE_KEY));
1919
2048
  if (view === void 0) return null;
1920
- if (view.slots.length === 0) return (0, react.createElement)("div", {
1921
- style: SECTION_STYLE$1,
1922
- "data-dsh-brief-sidebar": "memory-slots-section"
1923
- }, (0, react.createElement)("div", { style: HEADER_STYLE$2 }, t("section.memorySlots")), (0, react.createElement)("div", { style: EMPTY_STYLE$1 }, (0, react.createElement)("div", { style: {
2049
+ const referenced = view.session.map((entry) => entry.name);
2050
+ const rest = availableExcept(view.available, referenced);
2051
+ const latestNames = view.latest?.names ?? [];
2052
+ return (0, react.createElement)("div", {
2053
+ style: SECTION_STYLE$4,
2054
+ "data-dsh-brief-sidebar": "skill-usage-section"
2055
+ }, !hideHeader && (0, react.createElement)("div", { style: HEADER_STYLE$5 }, t("section.skillUsage")), view.session.length === 0 && (0, react.createElement)("div", { style: EMPTY_STYLE$3 }, (0, react.createElement)("div", { style: {
1924
2056
  marginBottom: 6,
1925
2057
  color: "var(--dsw-alias-label-secondary)"
1926
- } }, t("slots.empty")), (0, react.createElement)("div", null, t("slots.emptyHint"))));
1927
- return (0, react.createElement)("div", {
1928
- style: SECTION_STYLE$1,
1929
- "data-dsh-brief-sidebar": "memory-slots-section"
1930
- }, (0, react.createElement)("div", { style: HEADER_STYLE$2 }, t("section.memorySlots")), (0, react.createElement)("ul", { style: LIST_STYLE$1 }, view.slots.map((slot) => (0, react.createElement)(SlotRow, {
1931
- key: slot.id,
1932
- slot,
1933
- t
1934
- }))), (0, react.createElement)("div", { style: TOTAL_STYLE }, expandCount(t("slots.openCount"), view.openCount)));
2058
+ } }, t("skillUsage.empty")), (0, react.createElement)("div", null, t("skillUsage.emptyHint"))), view.session.length > 0 && (0, react.createElement)(GroupHeader, {
2059
+ text: expandCount(t("skillUsage.invokedGroup"), view.session.length),
2060
+ marker: "skill-usage-invoked-group"
2061
+ }), (0, react.createElement)("ul", { style: LIST_STYLE$3 }, view.session.map((entry) => (0, react.createElement)("li", {
2062
+ key: entry.name,
2063
+ style: ROW_STYLE$4,
2064
+ "data-dsh-brief-sidebar": "skill-usage-row"
2065
+ }, (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, {
2066
+ label: expandCount(t("skillUsage.availableGroup"), rest.length),
2067
+ open: availableOpen,
2068
+ marker: "skill-usage-available-toggle",
2069
+ onToggle: () => {
2070
+ setAvailableOpen((current) => !current);
2071
+ }
2072
+ }), rest.length > 0 && view.availableCount > view.available.length && (0, react.createElement)(GroupCaption, {
2073
+ text: expandCount(t("skillUsage.availableCapped"), view.availableCount),
2074
+ marker: "skill-usage-available-capped"
2075
+ }), availableOpen && (0, react.createElement)("ul", { style: LIST_STYLE$3 }, rest.map((name) => (0, react.createElement)("li", {
2076
+ key: name,
2077
+ style: ROW_STYLE$4,
2078
+ "data-dsh-brief-sidebar": "skill-usage-available-row"
2079
+ }, (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)));
1935
2080
  }
1936
2081
  //#endregion
1937
- //#region src/client/summary/memory-recall.ts
2082
+ //#region src/client/summary/SearchRefSection.tsx
1938
2083
  /**
1939
- * The `memoryRecall` projection — reading it.
2084
+ * The search-reference section: the `dshSummarySearchRefs` projection drawn as
2085
+ * one tab of the references section.
1940
2086
  *
1941
- * The projection unit is registered by `dsh-prime-memory` (key `memoryRecall`,
1942
- * wire view `{rev, sessionId, count, items[]}`): the memories this session has
1943
- * actually injected. Unlike
1944
- * `memorySlots` (pinned, resident conventions written by hand), this set is
1945
- * NOT resident — it is the at-a-glance answer to "what did the model see".
1946
- * It survives compaction on purpose: `compact` only marks the entries whose
1947
- * text left the context as `superseded` (and `/clear` empties the set), so a
1948
- * retired row keeps its strike-through instead of vanishing.
2087
+ * It answers "what did this session pull in from outside": the queries it ran
2088
+ * through `advanced_search`, and the sources those searches returned. Two
2089
+ * groups, because they are two different facts — a query says what was asked,
2090
+ * a source says what came back, and an older `dsh-free-search` without
2091
+ * `presentationMeta` produces queries with no sources at all. That degraded
2092
+ * case is stated rather than hidden, because an empty source list would
2093
+ * otherwise read as "the search found nothing".
1949
2094
  *
1950
- * This plugin does NOT depend on prime-memory: with the unit unregistered the
1951
- * client reads `undefined` from the face and the section hides. The module is
1952
- * deliberately PURE — no React, no context, no IO — so the narrowing is
1953
- * testable directly, exactly like `memory-slots.ts`.
2095
+ * Optional on both sides: no registry, or a host half that never registered the
2096
+ * unit, means the client reads capability absence and the section hides.
2097
+ *
2098
+ * Two contracts inherited from the recall section:
2099
+ * - **no native `title` attribute** — the whole text rides the row or is left
2100
+ * out; an unskinable tooltip would overlap the plugin's own surfaces;
2101
+ * - **colours from `--dsw-alias-*` tokens only** (skin contract).
2102
+ *
2103
+ * Source links are untrusted external data, so only `http`/`https` become
2104
+ * anchors (see `safeHref`) and they open with `rel="noreferrer noopener"`.
1954
2105
  */
1955
- /** The projection key `dsh-prime-memory` registers (its canonical constant). */
1956
- const MEMORY_RECALL_KEY = "memoryRecall";
1957
- /** Whether a value is a non-negative safe integer (the view's counter shape). */
1958
- function isCounter(value) {
1959
- return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
1960
- }
1961
- /** Narrow one item entry; `undefined` = malformed, dropped. */
1962
- function readItem(value) {
1963
- if (value === null || typeof value !== "object" || Array.isArray(value)) return void 0;
1964
- const raw = value;
1965
- if (typeof raw["id"] !== "string" || raw["id"].length === 0) return void 0;
1966
- if (typeof raw["type"] !== "string" || typeof raw["title"] !== "string") return void 0;
1967
- if (typeof raw["priority"] !== "number") return void 0;
1968
- const item = {
1969
- id: raw["id"],
1970
- type: raw["type"],
1971
- title: raw["title"],
1972
- priority: raw["priority"]
1973
- };
1974
- if (Array.isArray(raw["scenario"])) {
1975
- const phrases = raw["scenario"].filter((s) => typeof s === "string" && s.length > 0);
1976
- if (phrases.length > 0) item.scenario = phrases;
1977
- }
1978
- const rating = raw["rating"];
1979
- if (typeof rating === "number" && Number.isInteger(rating) && rating >= -1 && rating <= 5) item.rating = rating;
1980
- if (typeof raw["body"] === "string") item.body = raw["body"];
1981
- if (raw["retired"] === true) item.retired = true;
1982
- if (raw["superseded"] === true) item.superseded = true;
1983
- return item;
1984
- }
2106
+ const SECTION_STYLE$3 = {
2107
+ flexShrink: 0,
2108
+ borderBottom: "1px solid var(--dsw-alias-border-secondary)"
2109
+ };
2110
+ const HEADER_STYLE$4 = {
2111
+ padding: "10px 12px 0",
2112
+ color: "var(--dsw-alias-label-secondary)",
2113
+ fontSize: 12,
2114
+ fontWeight: 600
2115
+ };
2116
+ const LIST_STYLE$2 = {
2117
+ listStyle: "none",
2118
+ margin: 0,
2119
+ padding: "6px 0",
2120
+ display: "flex",
2121
+ flexDirection: "column"
2122
+ };
2123
+ const ROW_STYLE$3 = {
2124
+ display: "flex",
2125
+ alignItems: "flex-start",
2126
+ gap: 8,
2127
+ padding: "6px 12px"
2128
+ };
2129
+ const QUERY_STYLE = {
2130
+ minWidth: 0,
2131
+ flex: "1 1 auto",
2132
+ overflowWrap: "anywhere",
2133
+ color: "var(--dsw-alias-label-secondary)"
2134
+ };
2135
+ /** The source title, one line: the host and URL carry the identification. */
2136
+ const TITLE_STYLE$2 = {
2137
+ minWidth: 0,
2138
+ flex: "1 1 auto",
2139
+ display: "-webkit-box",
2140
+ WebkitLineClamp: 2,
2141
+ WebkitBoxOrient: "vertical",
2142
+ overflow: "hidden",
2143
+ color: "var(--dsw-alias-label-secondary)"
2144
+ };
2145
+ const SNIPPET_STYLE = {
2146
+ margin: 0,
2147
+ minWidth: 0,
2148
+ overflowWrap: "anywhere",
2149
+ color: "var(--dsw-alias-label-tertiary)",
2150
+ fontSize: 11,
2151
+ lineHeight: "16px"
2152
+ };
2153
+ const META_STYLE$2 = {
2154
+ flexShrink: 0,
2155
+ color: "var(--dsw-alias-label-tertiary)",
2156
+ fontSize: 11,
2157
+ lineHeight: "18px",
2158
+ whiteSpace: "nowrap"
2159
+ };
2160
+ /** The "本轮" pill: same form as the skill tag, tertiary emphasis. */
2161
+ const TAG_STYLE = {
2162
+ flexShrink: 0,
2163
+ color: "var(--dsw-alias-label-tertiary)",
2164
+ border: "1px solid var(--dsw-alias-label-tertiary)",
2165
+ borderRadius: 999,
2166
+ padding: "0 6px",
2167
+ fontSize: 11,
2168
+ lineHeight: "16px",
2169
+ whiteSpace: "nowrap"
2170
+ };
2171
+ const FOOTER_STYLE$2 = {
2172
+ padding: "0 12px 10px",
2173
+ color: "var(--dsw-alias-label-tertiary)",
2174
+ fontSize: 11
2175
+ };
2176
+ const EMPTY_STYLE$2 = {
2177
+ margin: "0 auto",
2178
+ padding: 24,
2179
+ maxWidth: 460,
2180
+ textAlign: "center",
2181
+ color: "var(--dsw-alias-label-tertiary)",
2182
+ lineHeight: 1.7,
2183
+ fontSize: 12
2184
+ };
1985
2185
  /**
1986
- * Narrow a raw projection value into a recall view.
1987
- *
1988
- * `undefined` means the capability is ABSENT (prime-memory not in the
1989
- * composition, or no frame carried the key) or the frame drifted beyond repair;
1990
- * a view with `items: []` IS the honest empty state (prime-memory publishes as
1991
- * soon as the unit registers). Malformed items are dropped, never thrown on.
2186
+ * The search-reference section. The subscription is unconditional (a plain
2187
+ * listener, not IO); the view decides whether anything is drawn.
1992
2188
  *
1993
- * @param value - the raw `unknown` snapshot of the `memoryRecall` face.
1994
- * @returns the validated view, or `undefined` when the capability is absent.
2189
+ * @param props - translation, client context, session scope, header switch.
1995
2190
  */
1996
- function readRecall(value) {
1997
- if (value === null || typeof value !== "object" || Array.isArray(value)) return void 0;
1998
- const raw = value;
1999
- if (!isCounter(raw["rev"]) || !isCounter(raw["count"])) return void 0;
2000
- if (typeof raw["sessionId"] !== "string") return void 0;
2001
- if (!Array.isArray(raw["items"])) return void 0;
2002
- const items = [];
2003
- for (const entry of raw["items"]) {
2004
- const item = readItem(entry);
2005
- if (item !== void 0) items.push(item);
2006
- }
2007
- return {
2008
- rev: raw["rev"],
2009
- sessionId: raw["sessionId"],
2010
- count: raw["count"],
2011
- items
2012
- };
2191
+ function SearchRefSection(props) {
2192
+ const { t, ctx, scope, hideHeader = false } = props;
2193
+ const view = readSearchRefs(useProjectionValue(ctx, scope?.sessionId, SEARCH_REFS_KEY));
2194
+ if (view === void 0) return null;
2195
+ if (view.queries.length === 0 && view.sources.length === 0) return (0, react.createElement)("div", {
2196
+ style: SECTION_STYLE$3,
2197
+ "data-dsh-brief-sidebar": "search-refs-section"
2198
+ }, !hideHeader && (0, react.createElement)("div", { style: HEADER_STYLE$4 }, t("section.searchRefs")), (0, react.createElement)("div", { style: EMPTY_STYLE$2 }, (0, react.createElement)("div", { style: {
2199
+ marginBottom: 6,
2200
+ color: "var(--dsw-alias-label-secondary)"
2201
+ } }, t("searchRefs.empty")), (0, react.createElement)("div", null, t("searchRefs.emptyHint"))));
2202
+ const body = [];
2203
+ if (view.queries.length > 0) {
2204
+ body.push((0, react.createElement)(GroupHeader, {
2205
+ key: "qh",
2206
+ text: expandCount(t("searchRefs.queryGroup"), view.queries.length),
2207
+ marker: "search-refs-query-group"
2208
+ }));
2209
+ body.push((0, react.createElement)("ul", {
2210
+ key: "ql",
2211
+ style: LIST_STYLE$2
2212
+ }, view.queries.map((row) => (0, react.createElement)("li", {
2213
+ key: `${row.turn}:${row.query}`,
2214
+ style: ROW_STYLE$3,
2215
+ "data-dsh-brief-sidebar": "search-refs-query"
2216
+ }, (0, react.createElement)("span", { style: QUERY_STYLE }, row.query), (0, react.createElement)("span", { style: META_STYLE$2 }, expandCount(t("searchRefs.hits"), row.hits))))));
2217
+ }
2218
+ if (view.sources.length > 0) {
2219
+ body.push((0, react.createElement)(GroupHeader, {
2220
+ key: "sh",
2221
+ text: expandCount(t("searchRefs.sourceGroup"), view.sources.length),
2222
+ marker: "search-refs-source-group"
2223
+ }));
2224
+ body.push((0, react.createElement)("ul", {
2225
+ key: "sl",
2226
+ style: LIST_STYLE$2
2227
+ }, view.sources.map((item) => {
2228
+ const href = safeHref(item.url);
2229
+ const label = item.title ?? item.url;
2230
+ const link = href === void 0 ? (0, react.createElement)("span", { style: TITLE_STYLE$2 }, label) : (0, react.createElement)("a", {
2231
+ style: TITLE_STYLE$2,
2232
+ href,
2233
+ target: "_blank",
2234
+ rel: "noreferrer noopener"
2235
+ }, label);
2236
+ return (0, react.createElement)("li", {
2237
+ key: item.url,
2238
+ style: ROW_STYLE$3,
2239
+ "data-dsh-brief-sidebar": "search-refs-source"
2240
+ }, (0, react.createElement)("div", { style: {
2241
+ minWidth: 0,
2242
+ flex: "1 1 auto",
2243
+ display: "flex",
2244
+ flexDirection: "column",
2245
+ gap: 2
2246
+ } }, 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")));
2247
+ })));
2248
+ } else body.push((0, react.createElement)("div", {
2249
+ key: "ns",
2250
+ style: FOOTER_STYLE$2,
2251
+ "data-dsh-brief-sidebar": "search-refs-no-sources"
2252
+ }, t("searchRefs.noSourcesHint")));
2253
+ if (view.queryTotal > view.queries.length || view.sourceTotal > view.sources.length) body.push((0, react.createElement)("div", {
2254
+ key: "cap",
2255
+ style: FOOTER_STYLE$2,
2256
+ "data-dsh-brief-sidebar": "search-refs-capped"
2257
+ }, expandTotal(t("searchRefs.capped"), Math.max(view.queryTotal, view.sourceTotal))));
2258
+ return (0, react.createElement)("div", {
2259
+ style: SECTION_STYLE$3,
2260
+ "data-dsh-brief-sidebar": "search-refs-section"
2261
+ }, !hideHeader && (0, react.createElement)("div", { style: HEADER_STYLE$4 }, t("section.searchRefs")), ...body);
2013
2262
  }
2014
2263
  //#endregion
2015
2264
  //#region src/client/summary/recall-feedback.ts
@@ -2236,7 +2485,7 @@ window.__ModuleLoader__.load({
2236
2485
  fontSize: 11,
2237
2486
  lineHeight: "16px"
2238
2487
  };
2239
- const BODY_STYLE$1 = {
2488
+ const BODY_STYLE$2 = {
2240
2489
  minWidth: 0,
2241
2490
  overflowWrap: "anywhere",
2242
2491
  whiteSpace: "pre-wrap",
@@ -2258,7 +2507,7 @@ window.__ModuleLoader__.load({
2258
2507
  };
2259
2508
  /** A retired record stays readable in the bubble, just marked. */
2260
2509
  const STALE_BODY_STYLE = {
2261
- ...BODY_STYLE$1,
2510
+ ...BODY_STYLE$2,
2262
2511
  textDecoration: "line-through"
2263
2512
  };
2264
2513
  /**
@@ -2285,7 +2534,7 @@ window.__ModuleLoader__.load({
2285
2534
  },
2286
2535
  "data-dsh-brief-sidebar": "recall-bubble",
2287
2536
  role: "tooltip"
2288
- }, (0, react.createElement)("span", { style: TYPE_STYLE$1 }, item.type), (0, react.createElement)("div", { style: stale ? STALE_BODY_STYLE : BODY_STYLE$1 }, item.body ?? `${item.title}\n${t("recall.noBody")}`), scenario !== void 0 && (0, react.createElement)("div", { style: SCENARIO_STYLE$1 }, scenario), (0, react.createElement)("div", { style: FOOTER_STYLE$1 }, foot));
2537
+ }, (0, react.createElement)("span", { style: TYPE_STYLE$1 }, item.type), (0, react.createElement)("div", { style: stale ? STALE_BODY_STYLE : BODY_STYLE$2 }, item.body ?? `${item.title}\n${t("recall.noBody")}`), scenario !== void 0 && (0, react.createElement)("div", { style: SCENARIO_STYLE$1 }, scenario), (0, react.createElement)("div", { style: FOOTER_STYLE$1 }, foot));
2289
2538
  }
2290
2539
  //#endregion
2291
2540
  //#region src/client/summary/MemoryRecallSection.tsx
@@ -2331,11 +2580,11 @@ window.__ModuleLoader__.load({
2331
2580
  * Colours come from `--dsw-alias-*` tokens only (skin contract, enforced by
2332
2581
  * `tests/purity.spec.ts`).
2333
2582
  */
2334
- const SECTION_STYLE = {
2583
+ const SECTION_STYLE$2 = {
2335
2584
  flexShrink: 0,
2336
2585
  borderBottom: "1px solid var(--dsw-alias-border-secondary)"
2337
2586
  };
2338
- const HEADER_STYLE$1 = {
2587
+ const HEADER_STYLE$3 = {
2339
2588
  padding: "10px 12px 0",
2340
2589
  color: "var(--dsw-alias-label-secondary)",
2341
2590
  fontSize: 12,
@@ -2347,14 +2596,14 @@ window.__ModuleLoader__.load({
2347
2596
  fontSize: 11,
2348
2597
  lineHeight: "15px"
2349
2598
  };
2350
- const LIST_STYLE = {
2599
+ const LIST_STYLE$1 = {
2351
2600
  listStyle: "none",
2352
2601
  margin: 0,
2353
2602
  padding: "6px 0",
2354
2603
  display: "flex",
2355
2604
  flexDirection: "column"
2356
2605
  };
2357
- const ROW_STYLE$1 = {
2606
+ const ROW_STYLE$2 = {
2358
2607
  display: "flex",
2359
2608
  alignItems: "flex-start",
2360
2609
  gap: 8,
@@ -2362,7 +2611,7 @@ window.__ModuleLoader__.load({
2362
2611
  };
2363
2612
  /** Selected row: one hairline of accent, no colour literal (skin contract). */
2364
2613
  const ROW_SELECTED_STYLE = {
2365
- ...ROW_STYLE$1,
2614
+ ...ROW_STYLE$2,
2366
2615
  background: "var(--dsw-alias-fill-quaternary)"
2367
2616
  };
2368
2617
  /** The type pill: same form as the slot kind pill, tertiary emphasis. */
@@ -2376,7 +2625,7 @@ window.__ModuleLoader__.load({
2376
2625
  lineHeight: "16px",
2377
2626
  whiteSpace: "nowrap"
2378
2627
  };
2379
- const TITLE_STYLE = {
2628
+ const TITLE_STYLE$1 = {
2380
2629
  minWidth: 0,
2381
2630
  color: "var(--dsw-alias-label-secondary)",
2382
2631
  display: "-webkit-box",
@@ -2402,7 +2651,7 @@ window.__ModuleLoader__.load({
2402
2651
  fontSize: 11,
2403
2652
  lineHeight: "16px"
2404
2653
  };
2405
- const META_STYLE = {
2654
+ const META_STYLE$1 = {
2406
2655
  flexShrink: 0,
2407
2656
  color: "var(--dsw-alias-label-tertiary)",
2408
2657
  fontSize: 11,
@@ -2414,7 +2663,7 @@ window.__ModuleLoader__.load({
2414
2663
  color: "var(--dsw-alias-label-tertiary)",
2415
2664
  fontSize: 12
2416
2665
  };
2417
- const EMPTY_STYLE = {
2666
+ const EMPTY_STYLE$1 = {
2418
2667
  margin: "0 auto",
2419
2668
  padding: 24,
2420
2669
  maxWidth: 460,
@@ -2530,11 +2779,11 @@ window.__ModuleLoader__.load({
2530
2779
  const staleText = stale && mark !== -1 ? t("recall.obsoleted") : void 0;
2531
2780
  const selectable = props.rating && !stale;
2532
2781
  const titleStyle = stale ? {
2533
- ...TITLE_STYLE,
2782
+ ...TITLE_STYLE$1,
2534
2783
  ...STRIKE_STYLE
2535
- } : TITLE_STYLE;
2784
+ } : TITLE_STYLE$1;
2536
2785
  return (0, react.createElement)("li", {
2537
- style: selected && props.rating ? ROW_SELECTED_STYLE : ROW_STYLE$1,
2786
+ style: selected && props.rating ? ROW_SELECTED_STYLE : ROW_STYLE$2,
2538
2787
  onClick: selectable ? props.onSelect : void 0,
2539
2788
  onMouseEnter: (e) => {
2540
2789
  const el = e.currentTarget;
@@ -2563,7 +2812,7 @@ window.__ModuleLoader__.load({
2563
2812
  confirmLabel: t("recall.obsoleteConfirm"),
2564
2813
  whyLabel: t("recall.obsoleteWhy"),
2565
2814
  onScore: props.onScore
2566
- })), (0, react.createElement)("span", { style: META_STYLE }, `P${item.priority}`));
2815
+ })), (0, react.createElement)("span", { style: META_STYLE$1 }, `P${item.priority}`));
2567
2816
  }
2568
2817
  /** How long the `-1` button stays armed before reverting to its plain label. */
2569
2818
  const ARM_WINDOW_MS = 5e3;
@@ -2669,10 +2918,10 @@ window.__ModuleLoader__.load({
2669
2918
  const historyRows = view.items.filter((item) => item.superseded === true && item.retired !== true);
2670
2919
  const hidden = historyRows.length;
2671
2920
  if (rows.length === 0) return (0, react.createElement)("div", {
2672
- style: SECTION_STYLE,
2921
+ style: SECTION_STYLE$2,
2673
2922
  "data-dsh-brief-sidebar": "memory-recall-section"
2674
- }, (0, react.createElement)("div", { style: HEADER_STYLE$1 }, t("section.memoryRecall")), (0, react.createElement)("div", {
2675
- style: EMPTY_STYLE,
2923
+ }, (0, react.createElement)("div", { style: HEADER_STYLE$3 }, t("section.memoryRecall")), (0, react.createElement)("div", {
2924
+ style: EMPTY_STYLE$1,
2676
2925
  ...hidden > 0 ? { "data-dsh-brief-sidebar": "recall-superseded-note" } : {}
2677
2926
  }, hidden > 0 ? expandCount(t("recall.supersededHidden"), hidden) : t("recall.empty")), historyRows.length > 0 && (0, react.createElement)(FoldToggle, {
2678
2927
  label: expandCount(t("recall.historyGroup"), historyRows.length),
@@ -2681,7 +2930,7 @@ window.__ModuleLoader__.load({
2681
2930
  onToggle: () => {
2682
2931
  setHistoryOpen((current) => !current);
2683
2932
  }
2684
- }), historyOpen && (0, react.createElement)("ul", { style: LIST_STYLE }, historyRows.map((item) => (0, react.createElement)(RecallRow, {
2933
+ }), historyOpen && (0, react.createElement)("ul", { style: LIST_STYLE$1 }, historyRows.map((item) => (0, react.createElement)(RecallRow, {
2685
2934
  key: item.id,
2686
2935
  item,
2687
2936
  selected: false,
@@ -2699,12 +2948,12 @@ window.__ModuleLoader__.load({
2699
2948
  onLeave: () => {}
2700
2949
  }))));
2701
2950
  return (0, react.createElement)("div", {
2702
- style: SECTION_STYLE,
2951
+ style: SECTION_STYLE$2,
2703
2952
  "data-dsh-brief-sidebar": "memory-recall-section"
2704
- }, (0, react.createElement)("div", { style: HEADER_STYLE$1 }, t("section.memoryRecall")), rating && (0, react.createElement)("div", { style: HINT_STYLE$1 }, `${t("recall.rateHint")}${t("recall.strikeHint")}`), rows.length > 0 && (0, react.createElement)(GroupHeader, {
2953
+ }, (0, react.createElement)("div", { style: HEADER_STYLE$3 }, t("section.memoryRecall")), rating && (0, react.createElement)("div", { style: HINT_STYLE$1 }, `${t("recall.rateHint")}${t("recall.strikeHint")}`), rows.length > 0 && (0, react.createElement)(GroupHeader, {
2705
2954
  text: expandCount(t("recall.activeGroup"), rows.length),
2706
2955
  marker: "recall-active-group"
2707
- }), (0, react.createElement)("ul", { style: LIST_STYLE }, rows.map((item) => (0, react.createElement)(RecallRow, {
2956
+ }), (0, react.createElement)("ul", { style: LIST_STYLE$1 }, rows.map((item) => (0, react.createElement)(RecallRow, {
2708
2957
  key: item.id,
2709
2958
  item,
2710
2959
  selected: selectedId === item.id,
@@ -2733,7 +2982,7 @@ window.__ModuleLoader__.load({
2733
2982
  onToggle: () => {
2734
2983
  setHistoryOpen((current) => !current);
2735
2984
  }
2736
- }), historyOpen && (0, react.createElement)("ul", { style: LIST_STYLE }, historyRows.map((item) => (0, react.createElement)(RecallRow, {
2985
+ }), historyOpen && (0, react.createElement)("ul", { style: LIST_STYLE$1 }, historyRows.map((item) => (0, react.createElement)(RecallRow, {
2737
2986
  key: item.id,
2738
2987
  item,
2739
2988
  selected: false,
@@ -2760,16 +3009,361 @@ window.__ModuleLoader__.load({
2760
3009
  })());
2761
3010
  }
2762
3011
  //#endregion
3012
+ //#region src/client/summary/MemorySlotsSection.tsx
3013
+ /**
3014
+ * The memory-slots section: the `memorySlots` projection drawn as the third
3015
+ * part of the summary tab (fixed last: 进展 → 产物 → 记忆).
3016
+ *
3017
+ * TWO gates, both fail-open to "hidden", so a host without dsh-prime-memory
3018
+ * paints nothing here:
3019
+ *
3020
+ * 1. **The switch.** This plugin's entry config (`configForms` → entry
3021
+ * `dsh-brief-sidebar`, field `showMemorySlots`, default OFF — the pinned
3022
+ * list moved out of the summary pane's way in favour of the session-recall
3023
+ * section). When the config face cannot answer (service absent, scope not
3024
+ * ready, field unset) the default applies — see `scope-face.ts`.
3025
+ * 2. **The capability.** The `memorySlots` projection must exist for the
3026
+ * session; `undefined` hides the section entirely, exactly like the
3027
+ * optional deliverables section.
3028
+ *
3029
+ * Colours come from `--dsw-alias-*` tokens only (skin contract, enforced by
3030
+ * `tests/purity.spec.ts`).
3031
+ */
3032
+ const SECTION_STYLE$1 = {
3033
+ flexShrink: 0,
3034
+ borderBottom: "1px solid var(--dsw-alias-border-secondary)"
3035
+ };
3036
+ const HEADER_STYLE$2 = {
3037
+ padding: "10px 12px 0",
3038
+ color: "var(--dsw-alias-label-secondary)",
3039
+ fontSize: 12,
3040
+ fontWeight: 600
3041
+ };
3042
+ const LIST_STYLE = {
3043
+ listStyle: "none",
3044
+ margin: 0,
3045
+ padding: "6px 0",
3046
+ display: "flex",
3047
+ flexDirection: "column"
3048
+ };
3049
+ const ROW_STYLE$1 = {
3050
+ display: "flex",
3051
+ alignItems: "flex-start",
3052
+ gap: 8,
3053
+ padding: "6px 12px"
3054
+ };
3055
+ const TITLE_STYLE = {
3056
+ minWidth: 0,
3057
+ overflowWrap: "anywhere",
3058
+ color: "var(--dsw-alias-label-secondary)"
3059
+ };
3060
+ const META_STYLE = {
3061
+ flexShrink: 0,
3062
+ color: "var(--dsw-alias-label-tertiary)",
3063
+ fontSize: 11,
3064
+ lineHeight: "18px",
3065
+ whiteSpace: "nowrap"
3066
+ };
3067
+ /** The kind pill: same form as the codeplan pill, tertiary emphasis. */
3068
+ const KIND_STYLE = {
3069
+ flexShrink: 0,
3070
+ color: "var(--dsw-alias-label-tertiary)",
3071
+ border: "1px solid var(--dsw-alias-label-tertiary)",
3072
+ borderRadius: 999,
3073
+ padding: "0 6px",
3074
+ fontSize: 11,
3075
+ lineHeight: "16px",
3076
+ whiteSpace: "nowrap"
3077
+ };
3078
+ /** The status pill: filled tint for an open slot, plain text otherwise. */
3079
+ const STATUS_OPEN_STYLE = {
3080
+ flexShrink: 0,
3081
+ color: "var(--dsw-alias-label-primary)",
3082
+ border: "1px solid var(--dsw-alias-label-secondary)",
3083
+ borderRadius: 999,
3084
+ padding: "0 6px",
3085
+ fontSize: 11,
3086
+ lineHeight: "16px",
3087
+ whiteSpace: "nowrap"
3088
+ };
3089
+ const STATUS_SETTLED_STYLE = {
3090
+ ...STATUS_OPEN_STYLE,
3091
+ color: "var(--dsw-alias-label-tertiary)",
3092
+ border: "1px solid var(--dsw-alias-label-tertiary)"
3093
+ };
3094
+ const BODY_STYLE$1 = {
3095
+ margin: 0,
3096
+ minWidth: 0,
3097
+ overflowWrap: "anywhere",
3098
+ color: "var(--dsw-alias-label-tertiary)",
3099
+ fontSize: 12,
3100
+ lineHeight: "18px"
3101
+ };
3102
+ const REFS_STYLE = {
3103
+ ...BODY_STYLE$1,
3104
+ fontSize: 11
3105
+ };
3106
+ const TOTAL_STYLE = {
3107
+ padding: "6px 12px 10px",
3108
+ color: "var(--dsw-alias-label-tertiary)",
3109
+ fontSize: 12
3110
+ };
3111
+ const EMPTY_STYLE = {
3112
+ margin: "0 auto",
3113
+ padding: 24,
3114
+ maxWidth: 460,
3115
+ textAlign: "center",
3116
+ color: "var(--dsw-alias-label-tertiary)",
3117
+ lineHeight: 1.7,
3118
+ fontSize: 12
3119
+ };
3120
+ /** Locale key per known slot status; unknown statuses fall back to the raw wire value. */
3121
+ const STATUS_KEY = {
3122
+ open: "slots.status.open",
3123
+ done: "slots.status.done",
3124
+ dropped: "slots.status.dropped",
3125
+ expired: "slots.status.expired"
3126
+ };
3127
+ /**
3128
+ * One slot row: kind pill, title (full body rides the tooltip), status pill,
3129
+ * priority; body line underneath when the wire view carries it; refs as
3130
+ * resolved 名称简述 (record_id → title) with the raw refs on the tooltip, or
3131
+ * raw refs when the host has not resolved them.
3132
+ */
3133
+ function SlotRow(props) {
3134
+ const { slot, t } = props;
3135
+ const statusLabel = STATUS_KEY[slot.status] === void 0 ? slot.status : t(STATUS_KEY[slot.status]);
3136
+ const title = slot.body === void 0 ? slot.title : `${slot.title} — ${slot.body}`;
3137
+ const refsLine = slot.refViews !== void 0 ? slot.refViews.map((view) => view.title).join(" · ") : slot.refs !== void 0 ? slot.refs.join(" · ") : void 0;
3138
+ return (0, react.createElement)("li", { style: ROW_STYLE$1 }, (0, react.createElement)("span", { style: KIND_STYLE }, slot.kind), (0, react.createElement)("div", { style: {
3139
+ minWidth: 0,
3140
+ flex: "1 1 auto",
3141
+ display: "flex",
3142
+ flexDirection: "column",
3143
+ gap: 2
3144
+ } }, (0, react.createElement)("span", {
3145
+ style: TITLE_STYLE,
3146
+ title
3147
+ }, slot.title), slot.body !== void 0 && (0, react.createElement)("p", { style: BODY_STYLE$1 }, truncateBody(slot.body)), refsLine !== void 0 && (0, react.createElement)("p", {
3148
+ style: REFS_STYLE,
3149
+ title: slot.refs !== void 0 ? slot.refs.join("\n") : void 0
3150
+ }, refsLine)), (0, react.createElement)("span", { style: META_STYLE }, `P${slot.priority}`), (0, react.createElement)("span", { style: slot.status === "open" ? STATUS_OPEN_STYLE : STATUS_SETTLED_STYLE }, statusLabel));
3151
+ }
3152
+ /**
3153
+ * Subscribe to this plugin's entry config and resolve the slots switch.
3154
+ * Plumbing lives in `use-config-flag`; the default (off) lives in the resolver.
3155
+ *
3156
+ * @param ctx - the client root context handed to the tab body.
3157
+ * @returns whether the memory-slots section may render at all.
3158
+ */
3159
+ function useShowMemorySlots(ctx) {
3160
+ return useConfigFlag(ctx, resolveShowMemorySlots);
3161
+ }
3162
+ /**
3163
+ * The memory-slots section. Both hooks run unconditionally (a conditional
3164
+ * hook order would break the moment the switch flips); the switch only
3165
+ * decides whether the resolved view paints.
3166
+ * @param props - translation, client context, session scope.
3167
+ */
3168
+ function MemorySlotsSection(props) {
3169
+ const { t, ctx, scope } = props;
3170
+ const show = useShowMemorySlots(ctx);
3171
+ const view = readSlots(useProjectionValue(ctx, scope?.sessionId, MEMORY_SLOTS_KEY));
3172
+ if (show === false) return null;
3173
+ if (view === void 0) return null;
3174
+ if (view.slots.length === 0) return (0, react.createElement)("div", {
3175
+ style: SECTION_STYLE$1,
3176
+ "data-dsh-brief-sidebar": "memory-slots-section"
3177
+ }, (0, react.createElement)("div", { style: HEADER_STYLE$2 }, t("section.memorySlots")), (0, react.createElement)("div", { style: EMPTY_STYLE }, (0, react.createElement)("div", { style: {
3178
+ marginBottom: 6,
3179
+ color: "var(--dsw-alias-label-secondary)"
3180
+ } }, t("slots.empty")), (0, react.createElement)("div", null, t("slots.emptyHint"))));
3181
+ return (0, react.createElement)("div", {
3182
+ style: SECTION_STYLE$1,
3183
+ "data-dsh-brief-sidebar": "memory-slots-section"
3184
+ }, (0, react.createElement)("div", { style: HEADER_STYLE$2 }, t("section.memorySlots")), (0, react.createElement)("ul", { style: LIST_STYLE }, view.slots.map((slot) => (0, react.createElement)(SlotRow, {
3185
+ key: slot.id,
3186
+ slot,
3187
+ t
3188
+ }))), (0, react.createElement)("div", { style: TOTAL_STYLE }, expandCount(t("slots.openCount"), view.openCount)));
3189
+ }
3190
+ //#endregion
3191
+ //#region src/client/summary/ReferenceSection.tsx
3192
+ /**
3193
+ * The references section: 技能与记忆并列成两个可左右切换的页签。
3194
+ *
3195
+ * Why a tab strip instead of three stacked sections: the two lists answer the
3196
+ * same question — "what entered this session's context" — from two different
3197
+ * providers (this plugin's own skill fold, and prime-memory's recall/slots).
3198
+ * Stacked, they pushed the board and the produced files off screen on a narrow
3199
+ * sidebar; side by side, each gets the full width when the reader actually
3200
+ * wants it.
3201
+ *
3202
+ * Two decisions worth knowing when editing this file:
3203
+ *
3204
+ * 1. **The inactive pane stays mounted and is hidden with `display:none`.**
3205
+ * The recall pane carries real interaction state (selected row, pending
3206
+ * rating, the expanded history group); unmounting it would silently reset
3207
+ * what the human just did every time they glanced at the skills tab. The
3208
+ * cost is that a hidden pane keeps re-rendering on projection frames —
3209
+ * bounded, because the whole tab still renders nothing while `visible` is
3210
+ * false (see `SummaryTab`).
3211
+ * 2. **The parent repeats its children's gates.** A section that hides itself
3212
+ * leaves the tab strip pointing at an empty pane, so the availability
3213
+ * question is answered here with the SAME readers and resolvers the children
3214
+ * use. `tests/reference-section.spec.tsx` pins both sides against the same
3215
+ * fixtures — if a child gate ever drifts from this one, that test goes red
3216
+ * rather than the UI growing a dead tab.
3217
+ *
3218
+ * Colours come from `--dsw-alias-*` tokens only (skin contract).
3219
+ */
3220
+ const SECTION_STYLE = {
3221
+ flexShrink: 0,
3222
+ borderBottom: "1px solid var(--dsw-alias-border-secondary)"
3223
+ };
3224
+ const HEADER_STYLE$1 = {
3225
+ padding: "10px 12px 0",
3226
+ color: "var(--dsw-alias-label-secondary)",
3227
+ fontSize: 12,
3228
+ fontWeight: 600
3229
+ };
3230
+ /** The tab strip: flush-left, same gutter as section headers. */
3231
+ const TABS_STYLE = {
3232
+ display: "flex",
3233
+ gap: 4,
3234
+ padding: "6px 12px 0"
3235
+ };
3236
+ /** One tab. Selected gets a hairline of secondary label; never a colour literal. */
3237
+ const TAB_STYLE = {
3238
+ background: "none",
3239
+ border: "none",
3240
+ borderBottom: "1px solid transparent",
3241
+ font: "inherit",
3242
+ fontSize: 12,
3243
+ color: "var(--dsw-alias-label-tertiary)",
3244
+ cursor: "pointer",
3245
+ padding: "4px 8px 6px"
3246
+ };
3247
+ const TAB_ACTIVE_STYLE = {
3248
+ ...TAB_STYLE,
3249
+ borderBottom: "1px solid var(--dsw-alias-label-secondary)",
3250
+ color: "var(--dsw-alias-label-primary)",
3251
+ fontWeight: 600
3252
+ };
3253
+ const ORDER = [
3254
+ "skills",
3255
+ "memory",
3256
+ "search"
3257
+ ];
3258
+ /**
3259
+ * The references section. All hooks run before any early return, so the tab
3260
+ * strip can disappear (no capability at all) without ever changing hook order.
3261
+ *
3262
+ * @param props - translation, client context, session scope, rating transport.
3263
+ */
3264
+ function ReferenceSection(props) {
3265
+ const { t, ctx, scope, fetchImpl } = props;
3266
+ const [tab, setTab] = (0, react.useState)("skills");
3267
+ const skillsLive = readSkillUsage(useProjectionValue(ctx, scope?.sessionId, SKILL_USAGE_KEY)) !== void 0;
3268
+ const searchLive = readSearchRefs(useProjectionValue(ctx, scope?.sessionId, SEARCH_REFS_KEY)) !== void 0;
3269
+ const recallFrame = readRecall(useProjectionValue(ctx, scope?.sessionId, MEMORY_RECALL_KEY));
3270
+ const slotsFrame = readSlots(useProjectionValue(ctx, scope?.sessionId, MEMORY_SLOTS_KEY));
3271
+ const recallOn = useConfigFlag(ctx, resolveShowMemoryRecall) !== false;
3272
+ const slotsOn = useConfigFlag(ctx, resolveShowMemorySlots) === true;
3273
+ const memoryLive = recallFrame !== void 0 && recallOn || slotsFrame !== void 0 && slotsOn;
3274
+ const live = {
3275
+ skills: skillsLive,
3276
+ memory: memoryLive,
3277
+ search: searchLive
3278
+ };
3279
+ const active = live[tab] ? tab : ORDER.find((id) => live[id]) ?? "skills";
3280
+ const onTabsKeyDown = (0, react.useCallback)((event) => {
3281
+ if (event.key !== "ArrowLeft" && event.key !== "ArrowRight") return;
3282
+ event.preventDefault();
3283
+ const shown = ORDER.filter((id) => live[id]);
3284
+ if (shown.length < 2) return;
3285
+ const at = shown.indexOf(active);
3286
+ const step = event.key === "ArrowRight" ? 1 : shown.length - 1;
3287
+ setTab(shown[(at + step) % shown.length]);
3288
+ }, [live, active]);
3289
+ if (!skillsLive && !memoryLive && !searchLive) return null;
3290
+ const tabButton = (id, label) => (0, react.createElement)("button", {
3291
+ key: id,
3292
+ type: "button",
3293
+ role: "tab",
3294
+ id: `dsh-brief-reference-tab-${id}`,
3295
+ "aria-selected": active === id,
3296
+ "aria-controls": `dsh-brief-reference-pane-${id}`,
3297
+ style: active === id ? TAB_ACTIVE_STYLE : TAB_STYLE,
3298
+ "data-dsh-brief-sidebar": `reference-tab-${id}`,
3299
+ onClick: () => {
3300
+ setTab(id);
3301
+ }
3302
+ }, label);
3303
+ return (0, react.createElement)("div", {
3304
+ style: SECTION_STYLE,
3305
+ "data-dsh-brief-sidebar": "references-section"
3306
+ }, (0, react.createElement)("div", { style: HEADER_STYLE$1 }, t("section.references")), (0, react.createElement)("div", {
3307
+ role: "tablist",
3308
+ "aria-label": t("section.references"),
3309
+ style: TABS_STYLE,
3310
+ onKeyDown: onTabsKeyDown,
3311
+ "data-dsh-brief-sidebar": "reference-tabs"
3312
+ }, skillsLive && tabButton("skills", t("references.tabSkills")), memoryLive && tabButton("memory", t("references.tabMemory")), searchLive && tabButton("search", t("references.tabSearch"))), skillsLive && (0, react.createElement)("div", {
3313
+ role: "tabpanel",
3314
+ id: "dsh-brief-reference-pane-skills",
3315
+ "aria-labelledby": "dsh-brief-reference-tab-skills",
3316
+ hidden: active !== "skills",
3317
+ style: active === "skills" ? void 0 : { display: "none" },
3318
+ "data-dsh-brief-sidebar": "reference-pane-skills"
3319
+ }, (0, react.createElement)(SkillUsageSection, {
3320
+ t,
3321
+ ctx,
3322
+ scope,
3323
+ hideHeader: true
3324
+ })), memoryLive && (0, react.createElement)("div", {
3325
+ role: "tabpanel",
3326
+ id: "dsh-brief-reference-pane-memory",
3327
+ "aria-labelledby": "dsh-brief-reference-tab-memory",
3328
+ hidden: active !== "memory",
3329
+ style: active === "memory" ? void 0 : { display: "none" },
3330
+ "data-dsh-brief-sidebar": "reference-pane-memory"
3331
+ }, (0, react.createElement)(MemoryRecallSection, {
3332
+ t,
3333
+ ctx,
3334
+ scope,
3335
+ fetchImpl
3336
+ }), (0, react.createElement)(MemorySlotsSection, {
3337
+ t,
3338
+ ctx,
3339
+ scope
3340
+ })), searchLive && (0, react.createElement)("div", {
3341
+ role: "tabpanel",
3342
+ id: "dsh-brief-reference-pane-search",
3343
+ "aria-labelledby": "dsh-brief-reference-tab-search",
3344
+ hidden: active !== "search",
3345
+ style: active === "search" ? void 0 : { display: "none" },
3346
+ "data-dsh-brief-sidebar": "reference-pane-search"
3347
+ }, (0, react.createElement)(SearchRefSection, {
3348
+ t,
3349
+ ctx,
3350
+ scope,
3351
+ hideHeader: true
3352
+ })));
3353
+ }
3354
+ //#endregion
2763
3355
  //#region src/client/SummaryTab.tsx
2764
3356
  /**
2765
3357
  * The summary tab body: the session's at-a-glance state, composed of sections.
2766
3358
  *
2767
3359
  * Section order: 进展 (the todos board) → 产物 (produced files, codeplan
2768
- * artifacts marked inline) → 技能引用 (which skills this session loaded) →
2769
- * 本会话召回 (what prime-memory injected into this session and compaction has
2770
- * not overwritten) → 记忆槽位 (hand-pinned resident conventions, off by default).
2771
- * Each section owns its own data chain and its own gating contract; this
2772
- * component owns only the shell.
3360
+ * artifacts marked inline) → 引用 (two switchable tabs: 技能 and 记忆).
3361
+ * The reference tabs replaced three stacked sections: which skills entered the
3362
+ * session, what prime-memory injected and compaction has not overwritten, and
3363
+ * the hand-pinned resident slots — all three answer "what is in this session's
3364
+ * context", and stacking them pushed the board off a narrow sidebar. Each pane
3365
+ * keeps its own data chain and its own gating contract; this component owns
3366
+ * only the shell.
2773
3367
  *
2774
3368
  * Two better-sidebar contracts are honoured:
2775
3369
  *
@@ -2816,18 +3410,8 @@ window.__ModuleLoader__.load({
2816
3410
  t,
2817
3411
  ctx,
2818
3412
  scope
2819
- }), (0, react.createElement)(SkillUsageSection, {
2820
- key: "skill-usage",
2821
- t,
2822
- ctx,
2823
- scope
2824
- }), (0, react.createElement)(MemoryRecallSection, {
2825
- key: "memory-recall",
2826
- t,
2827
- ctx,
2828
- scope
2829
- }), (0, react.createElement)(MemorySlotsSection, {
2830
- key: "memory-slots",
3413
+ }), (0, react.createElement)(ReferenceSection, {
3414
+ key: "references",
2831
3415
  t,
2832
3416
  ctx,
2833
3417
  scope