dsh-brief-sidebar 0.7.0 → 0.9.0

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.
Files changed (36) hide show
  1. package/dsh.plugin.json +1 -1
  2. package/lib/client.js +1306 -92
  3. package/lib/client.js.map +1 -1
  4. package/lib/index.mjs +222 -16
  5. package/lib/index.mjs.map +1 -1
  6. package/lib/types/client/locales.d.ts +21 -0
  7. package/lib/types/client/scope-face.d.ts +18 -6
  8. package/lib/types/client/summary/MemoryRecallSection.d.ts +48 -0
  9. package/lib/types/client/summary/MemorySlotsSection.d.ts +2 -6
  10. package/lib/types/client/summary/RecallBubble.d.ts +45 -0
  11. package/lib/types/client/summary/SkillUsageSection.d.ts +19 -0
  12. package/lib/types/client/summary/memory-recall.d.ts +80 -0
  13. package/lib/types/client/summary/recall-feedback.d.ts +80 -0
  14. package/lib/types/client/summary/skill-usage.d.ts +42 -0
  15. package/lib/types/client/summary/use-config-flag.d.ts +10 -0
  16. package/lib/types/index.d.ts +23 -7
  17. package/lib/types/projection/keys.d.ts +33 -2
  18. package/lib/types/projection/register.d.ts +6 -0
  19. package/lib/types/projection/skill-usage-fold.d.ts +88 -0
  20. package/package.json +1 -1
  21. package/src/client/SummaryTab.tsx +14 -5
  22. package/src/client/locales.ts +546 -357
  23. package/src/client/scope-face.ts +26 -7
  24. package/src/client/settings-card.tsx +52 -15
  25. package/src/client/summary/MemoryRecallSection.tsx +564 -0
  26. package/src/client/summary/MemorySlotsSection.tsx +8 -46
  27. package/src/client/summary/RecallBubble.tsx +171 -0
  28. package/src/client/summary/SkillUsageSection.tsx +154 -0
  29. package/src/client/summary/memory-recall.ts +131 -0
  30. package/src/client/summary/recall-feedback.ts +164 -0
  31. package/src/client/summary/skill-usage.ts +85 -0
  32. package/src/client/summary/use-config-flag.ts +60 -0
  33. package/src/index.ts +25 -9
  34. package/src/projection/keys.ts +33 -2
  35. package/src/projection/register.ts +75 -12
  36. package/src/projection/skill-usage-fold.ts +164 -0
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$2 = {
234
+ const SECTION_STYLE$4 = {
235
235
  flexShrink: 0,
236
236
  borderBottom: "1px solid var(--dsw-alias-border-secondary)"
237
237
  };
238
- const HEADER_STYLE$3 = {
238
+ const HEADER_STYLE$5 = {
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$2 = {
249
+ const LIST_STYLE$4 = {
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$3 = {
256
+ const ROW_STYLE$5 = {
257
257
  display: "flex",
258
258
  alignItems: "flex-start",
259
259
  gap: 8,
@@ -305,7 +305,7 @@ window.__ModuleLoader__.load({
305
305
  /** One task row. */
306
306
  function TodoRow(props) {
307
307
  const { item, t } = props;
308
- return (0, react.createElement)("li", { style: ROW_STYLE$3 }, (0, react.createElement)("span", { style: pillStyle(item.status) }, t(statusKey(item.status))), (0, react.createElement)("span", { style: contentStyle(item.status) }, item.content));
308
+ return (0, react.createElement)("li", { style: ROW_STYLE$5 }, (0, react.createElement)("span", { style: pillStyle(item.status) }, t(statusKey(item.status))), (0, react.createElement)("span", { style: contentStyle(item.status) }, item.content));
309
309
  }
310
310
  /**
311
311
  * The progress section. The subscription is unconditional (a plain listener,
@@ -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$2,
319
+ style: SECTION_STYLE$4,
320
320
  "data-dsh-brief-sidebar": "progress-section"
321
- }, (0, react.createElement)("div", { style: HEADER_STYLE$3 }, t("section.progress")), (0, react.createElement)(Notice, {
321
+ }, (0, react.createElement)("div", { style: HEADER_STYLE$5 }, 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$2,
326
+ style: SECTION_STYLE$4,
327
327
  "data-dsh-brief-sidebar": "progress-section"
328
- }, (0, react.createElement)("div", { style: HEADER_STYLE$3 }, t("section.progress")), (0, react.createElement)(Notice, {
328
+ }, (0, react.createElement)("div", { style: HEADER_STYLE$5 }, 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$2,
334
+ style: SECTION_STYLE$4,
335
335
  "data-dsh-brief-sidebar": "progress-section"
336
- }, (0, react.createElement)("div", { style: HEADER_STYLE$3 }, t("section.progress")), (0, react.createElement)("div", { style: PROGRESS_STYLE }, progressLine(counts, t)), (0, react.createElement)("ul", { style: LIST_STYLE$2 }, todos.map((item, index) => (0, react.createElement)(TodoRow, {
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, {
337
337
  key: `${index}:${item.content}`,
338
338
  item,
339
339
  t
@@ -342,8 +342,8 @@ window.__ModuleLoader__.load({
342
342
  //#endregion
343
343
  //#region src/projection/keys.ts
344
344
  /**
345
- * The projection key this plugin contributes to the session-projection
346
- * registry, and its wire view.
345
+ * The projection keys this plugin contributes to the session-projection
346
+ * registry, and their wire views.
347
347
  *
348
348
  * The key is namespaced with the plugin name on purpose: the registry refuses
349
349
  * to share a key across differing `stateVersion`s, so a collision with a
@@ -354,6 +354,14 @@ window.__ModuleLoader__.load({
354
354
  */
355
355
  /** The projection key registered by this plugin's host half. */
356
356
  const DELIVERABLES_KEY = "dshSummaryDeliverables";
357
+ /**
358
+ * The second key: which skills this session actually loaded (the host's `skill`
359
+ * tool, read off `tool/call.arguments`). Namespaced for the same reason as
360
+ * {@link DELIVERABLES_KEY} — the registry hard-fails on a shared key with a
361
+ * differing `stateVersion`, so a future host-side skill unit must not be able
362
+ * to collide silently.
363
+ */
364
+ const SKILL_USAGE_KEY = "dshSummarySkillUsage";
357
365
  //#endregion
358
366
  //#region src/client/file-open.ts
359
367
  /** The address scheme every file resource opens with. */
@@ -561,10 +569,31 @@ window.__ModuleLoader__.load({
561
569
  "slots.status.done": "已完成",
562
570
  "slots.status.dropped": "已弃置",
563
571
  "slots.status.expired": "已过期",
572
+ "section.memoryRecall": "本会话召回",
573
+ "recall.empty": "本会话还没有召回到记忆",
574
+ "recall.count": "注入 {count} 条 · 被摘要取代的已折叠(清空会话才清除)",
575
+ "recall.rateHint": "点选一条后可标注:0–5 相关度;-1 声明过时并退场(可恢复)",
576
+ "recall.obsolete": "声明过时",
577
+ "recall.obsoleteConfirm": "确认退场?",
578
+ "recall.obsoleted": "已判过时",
579
+ "recall.rated": "已评 {count} 分",
580
+ "recall.failed": "标注失败:",
581
+ "recall.obsoleteWhy": "声明过时后,该记忆退场(可恢复),下一轮会告知模型",
582
+ "recall.strikeHint": ";判过时的条目会一直保留在列表里并划上删除线(清空会话才移除)",
583
+ "recall.supersededHidden": "已隐藏 {count} 条被摘要取代的旧召回(正文已不在上下文)",
584
+ "recall.noBody": "(本帧没有这条记忆的全文)",
585
+ "section.skillUsage": "技能引用",
586
+ "skillUsage.empty": "本会话还没载入过技能",
587
+ "skillUsage.emptyHint": "模型用 skill 工具载入技能后,技能名会实时出现在这里。",
588
+ "skillUsage.uses": "{count} 次",
589
+ "skillUsage.count": "载入 {count} 次 · {total} 个技能",
590
+ "skillUsage.latestTag": "本轮",
564
591
  "settings.title": "概要侧栏",
565
592
  "settings.desc": "右侧栏概要 tab 的分区开关(只读展示)。",
566
593
  "settings.memorySlots": "记忆槽位分区",
567
594
  "settings.memorySlotsHint": "开启后,概要 tab 在进展与产物之下显示 dsh-prime-memory 的激活记忆槽位(只读)。",
595
+ "settings.memoryRecall": "本会话召回分区",
596
+ "settings.memoryRecallHint": "开启后,概要 tab 显示本会话已注入的记忆,并可标注相关度(0–5;-1 声明过时并退场)。被摘要取代的条目会自动折叠。",
568
597
  "settings.unavailable": "配置面不可用,记忆分区按默认「开」处理。",
569
598
  "settings.readonly": "当前配置只读,无法在此修改。"
570
599
  },
@@ -592,10 +621,31 @@ window.__ModuleLoader__.load({
592
621
  "slots.status.done": "Done",
593
622
  "slots.status.dropped": "Dropped",
594
623
  "slots.status.expired": "Expired",
624
+ "section.memoryRecall": "Session recall",
625
+ "recall.empty": "No memories recalled in this session yet",
626
+ "recall.count": "{count} injected · superseded ones folded after compaction (cleared only on /clear)",
627
+ "recall.rateHint": "Select a memory to mark it: 0–5 relevance; -1 declares it obsolete and retires it (restorable).",
628
+ "recall.obsolete": "Obsolete",
629
+ "recall.obsoleteConfirm": "Retire it?",
630
+ "recall.obsoleted": "Marked obsolete",
631
+ "recall.rated": "Rated {count}/5",
632
+ "recall.failed": "Mark failed: ",
633
+ "recall.obsoleteWhy": "Declaring it obsolete retires the record (restorable) and tells the model next turn",
634
+ "recall.strikeHint": "; an entry you mark obsolete stays listed, struck through (removed only when you clear the session)",
635
+ "recall.supersededHidden": "{count} older recalls hidden — their text was replaced by a summary",
636
+ "recall.noBody": "(this frame carries no full body for this record)",
637
+ "section.skillUsage": "Skill references",
638
+ "skillUsage.empty": "No skills loaded in this session",
639
+ "skillUsage.emptyHint": "Skills appear here as soon as the model loads one with the skill tool.",
640
+ "skillUsage.uses": "{count}x",
641
+ "skillUsage.count": "{count} loads - {total} skills",
642
+ "skillUsage.latestTag": "this turn",
595
643
  "settings.title": "Brief sidebar",
596
644
  "settings.desc": "Section switches for the summary tab in the right sidebar (read-only views).",
597
645
  "settings.memorySlots": "Memory slots section",
598
646
  "settings.memorySlotsHint": "When on, the summary tab lists the active memory slots from dsh-prime-memory beneath progress and produced files (read-only).",
647
+ "settings.memoryRecall": "Session recall section",
648
+ "settings.memoryRecallHint": "When on, the Summary tab lists the memories injected this session and lets you rate them (0–5; -1 declares one obsolete and retires it). Entries a summary replaced are folded away.",
599
649
  "settings.unavailable": "The config face is unavailable; the memory section falls back to its default (on).",
600
650
  "settings.readonly": "This config is read-only here; edits are not available."
601
651
  },
@@ -623,10 +673,31 @@ window.__ModuleLoader__.load({
623
673
  "slots.status.done": "完了",
624
674
  "slots.status.dropped": "破棄",
625
675
  "slots.status.expired": "期限切れ",
676
+ "section.memoryRecall": "このセッションの想起",
677
+ "recall.empty": "このセッションではまだ記憶を想起していません",
678
+ "recall.count": "{count} 件を注入 · 要約に置き換わった分は折りたたみ(/clear でのみ消去)",
679
+ "recall.rateHint": "行を選ぶと採点できます:0–5 関連度;-1 は「古い」と宣言して退場(復元可)。",
680
+ "recall.obsolete": "古いと宣言",
681
+ "recall.obsoleteConfirm": "退場させますか?",
682
+ "recall.obsoleted": "古いと判定済み",
683
+ "recall.rated": "{count} 点",
684
+ "recall.failed": "採点に失敗:",
685
+ "recall.obsoleteWhy": "古いと宣言すると記録は退場(復元可)、次のターンでモデルに伝わります",
686
+ "recall.strikeHint": ";古いと宣言した項目は取り消し線のまま一覧に保持されます(セッションをクリアすると消えます)",
687
+ "recall.supersededHidden": "要約に置き換わった旧召回 {count} 件を非表示(本文はコンテキストにない)",
688
+ "recall.noBody": "このフレームには全文が含まれていない",
689
+ "section.skillUsage": "スキル参照",
690
+ "skillUsage.empty": "このセッションではスキルを読み込んでいません",
691
+ "skillUsage.emptyHint": "モデルが skill ツールでスキルを読み込むと、ここにリアルタイムで表示されます。",
692
+ "skillUsage.uses": "{count} 回",
693
+ "skillUsage.count": "読み込み {count} 回 · スキル {total} 件",
694
+ "skillUsage.latestTag": "このターン",
626
695
  "settings.title": "概要サイドバー",
627
696
  "settings.desc": "右サイドバー概要タブのセクション表示切り替え(読み取り専用)。",
628
697
  "settings.memorySlots": "メモリスロットセクション",
629
698
  "settings.memorySlotsHint": "オンにすると、概要タブの進行状況と成果物の下に dsh-prime-memory のアクティブなメモリスロット(読み取り専用)が表示されます。",
699
+ "settings.memoryRecall": "セッション想起のセクション",
700
+ "settings.memoryRecallHint": "有効にすると概要タブに本セッションで注入された記憶を表示し、関連度(0–5、-1 は陳腐化を宣言して退場)を付けられます。要約に置き換わった項目は自動で折りたたまれます。",
630
701
  "settings.unavailable": "設定画面を利用できません。メモリセクションは既定(オン)で動作します。",
631
702
  "settings.readonly": "この設定はここでは読み取り専用のため変更できません。"
632
703
  },
@@ -654,10 +725,31 @@ window.__ModuleLoader__.load({
654
725
  "slots.status.done": "완료",
655
726
  "slots.status.dropped": "폐기",
656
727
  "slots.status.expired": "만료",
728
+ "section.memoryRecall": "이 세션 회상",
729
+ "recall.empty": "이 세션에서는 아직 기억을 회상하지 않았습니다",
730
+ "recall.count": "{count}건 주입 · 요약으로 대체된 항목은 접힘(/clear 시에만 삭제)",
731
+ "recall.rateHint": "항목을 선택하면 평가할 수 있습니다: 0–5 관련도; -1은 오래된 것으로 선언하여 삭제(복원 가능).",
732
+ "recall.obsolete": "오래됨 선언",
733
+ "recall.obsoleteConfirm": "삭제할까요?",
734
+ "recall.obsoleted": "오래된 것으로 판정됨",
735
+ "recall.rated": "{count}점 평가됨",
736
+ "recall.failed": "평가 실패: ",
737
+ "recall.obsoleteWhy": "낡음으로 선언하면 기록이 퇴장하며(복원 가능), 다음 턴에서 모델에 알립니다",
738
+ "recall.strikeHint": ";낡음으로 선언한 항목은 취소선 상태로 목록에 계속 표시됩니다(세션을 비우면 제거)",
739
+ "recall.supersededHidden": "요약으로 대체된 이전 회수 {count}건 숨김(본문이 컨텍스트에 없음)",
740
+ "recall.noBody": "이 프레임에는 이 기록의 전문이 없습니다",
741
+ "section.skillUsage": "스킬 참조",
742
+ "skillUsage.empty": "이 세션에서 스킬을 불러오지 않았습니다",
743
+ "skillUsage.emptyHint": "모델이 skill 도구로 스킬을 불러오면 여기에 즉시 표시됩니다.",
744
+ "skillUsage.uses": "{count}회",
745
+ "skillUsage.count": "불러오기 {count}회 · 스킬 {total}개",
746
+ "skillUsage.latestTag": "이 턴",
657
747
  "settings.title": "요약 사이드바",
658
748
  "settings.desc": "오른쪽 사이드바 요약 탭의 섹션 표시 전환(읽기 전용).",
659
749
  "settings.memorySlots": "메모리 슬롯 섹션",
660
750
  "settings.memorySlotsHint": "켜면 요약 탭의 진행 상황과 산출물 아래에 dsh-prime-memory의 활성 메모리 슬롯(읽기 전용)이 표시됩니다.",
751
+ "settings.memoryRecall": "세션 회상 섹션",
752
+ "settings.memoryRecallHint": "켜면 요약 탭에 이번 세션에서 주입된 기억을 표시하고 관련도(0–5, -1은 노후 선언 후 퇴장)를 매길 수 있습니다. 요약으로 대체된 항목은 자동으로 접힙니다.",
661
753
  "settings.unavailable": "설정 화면을 사용할 수 없어 메모리 섹션은 기본값(켜짐)으로 동작합니다.",
662
754
  "settings.readonly": "이 설정은 여기서 읽기 전용이므로 변경할 수 없습니다."
663
755
  },
@@ -685,10 +777,31 @@ window.__ModuleLoader__.load({
685
777
  "slots.status.done": "Terminé",
686
778
  "slots.status.dropped": "Abandonné",
687
779
  "slots.status.expired": "Expiré",
780
+ "section.memoryRecall": "Rappel de la session",
781
+ "recall.empty": "Aucun souvenir rappelé dans cette session",
782
+ "recall.count": "{count} injectés · les remplacés par un résumé sont repliés après compression (effacés par /clear)",
783
+ "recall.rateHint": "Sélectionnez un souvenir pour le noter : 0–5 pertinence ; -1 le déclare obsolète et le retire (récupérable).",
784
+ "recall.obsolete": "Obsolète",
785
+ "recall.obsoleteConfirm": "Le retirer ?",
786
+ "recall.obsoleted": "Déclaré obsolète",
787
+ "recall.rated": "Noté {count}/5",
788
+ "recall.failed": "Échec de la note : ",
789
+ "recall.obsoleteWhy": "Déclarer obsolète retire l’enregistrement (récupérable) et prévient le modèle au tour suivant",
790
+ "recall.strikeHint": " ; une entrée déclarée obsolète reste listée, barrée (retirée uniquement quand vous videz la session)",
791
+ "recall.supersededHidden": "{count} anciens rappels masqués — leur texte a été remplacé par un résumé",
792
+ "recall.noBody": "(aucun texte complet pour cette entrée dans cette trame)",
793
+ "section.skillUsage": "Références de compétences",
794
+ "skillUsage.empty": "Aucune compétence chargée dans cette session",
795
+ "skillUsage.emptyHint": "Les compétences apparaissent ici dès que le modèle en charge une avec l’outil skill.",
796
+ "skillUsage.uses": "{count}×",
797
+ "skillUsage.count": "{count} chargements · {total} compétences",
798
+ "skillUsage.latestTag": "ce tour",
688
799
  "settings.title": "Barre latérale Résumé",
689
800
  "settings.desc": "Interrupteurs des sections de l’onglet Résumé de la barre latérale (lecture seule).",
690
801
  "settings.memorySlots": "Section des emplacements mémoire",
691
802
  "settings.memorySlotsHint": "Une fois activé, l’onglet Résumé liste les emplacements mémoire actifs de dsh-prime-memory sous l’avancement et les fichiers produits (lecture seule).",
803
+ "settings.memoryRecall": "Section rappel de session",
804
+ "settings.memoryRecallHint": "Actif, onglet Résumé : liste les souvenirs injectés dans cette session et permet de les noter (0–5 ; -1 déclare obsolète et retire). Les entrées remplacées par un résumé sont repliées.",
692
805
  "settings.unavailable": "Le canal de configuration est indisponible ; la section mémoire revient à sa valeur par défaut (activée).",
693
806
  "settings.readonly": "Cette configuration est en lecture seule ici ; aucune modification possible."
694
807
  },
@@ -716,10 +829,31 @@ window.__ModuleLoader__.load({
716
829
  "slots.status.done": "Erledigt",
717
830
  "slots.status.dropped": "Verworfen",
718
831
  "slots.status.expired": "Abgelaufen",
832
+ "section.memoryRecall": "Sitzungserinnerung",
833
+ "recall.empty": "In dieser Sitzung wurden noch keine Erinnerungen geholt",
834
+ "recall.count": "{count} eingefügt · die durch eine Zusammenfassung ersetzten sind nach der Komprimierung eingeklappt (bei /clear gelöscht)",
835
+ "recall.rateHint": "Ein Eintrag auswählen zum Bewerten: 0–5 Relevanz; -1 erklärt ihn für veraltet und entfernt ihn (wiederherstellbar).",
836
+ "recall.obsolete": "Veraltet",
837
+ "recall.obsoleteConfirm": "Entfernen?",
838
+ "recall.obsoleted": "Als veraltet markiert",
839
+ "recall.rated": "Bewertet {count}/5",
840
+ "recall.failed": "Bewertung fehlgeschlagen: ",
841
+ "recall.obsoleteWhy": "Als veraltet erklären versetzt den Eintrag in den Ruhestand (wiederherstellbar) und informiert das Modell in der nächsten Runde",
842
+ "recall.strikeHint": "; ein als veraltet markierter Eintrag bleibt durchgestrichen in der Liste (erst beim Leeren der Sitzung entfernt)",
843
+ "recall.supersededHidden": "{count} ältere Treffer ausgeblendet — ihr Text wurde durch eine Zusammenfassung ersetzt",
844
+ "recall.noBody": "(dieser Rahmen enthält keinen vollständigen Text für diesen Eintrag)",
845
+ "section.skillUsage": "Skill-Referenzen",
846
+ "skillUsage.empty": "In dieser Sitzung wurden keine Skills geladen",
847
+ "skillUsage.emptyHint": "Skills erscheinen hier, sobald das Modell einen über das skill-Werkzeug lädt.",
848
+ "skillUsage.uses": "{count}x",
849
+ "skillUsage.count": "{count} Ladungen · {total} Skills",
850
+ "skillUsage.latestTag": "diese Runde",
719
851
  "settings.title": "Übersicht-Seitenleiste",
720
852
  "settings.desc": "Schalter für die Abschnitte des Übersicht-Tabs in der rechten Seitenleiste (schreibgeschützt).",
721
853
  "settings.memorySlots": "Speicherplätze-Abschnitt",
722
854
  "settings.memorySlotsHint": "Ist der Schalter eingeschaltet, listet der Übersicht-Tab die aktiven Speicherplätze aus dsh-prime-memory unter Fortschritt und erzeugten Dateien auf (schreibgeschützt).",
855
+ "settings.memoryRecall": "Bereich Sitzungserinnerung",
856
+ "settings.memoryRecallHint": "Aktiv: Der Tab „Zusammenfassung“ listet die in dieser Sitzung eingefügten Erinnerungen und erlaubt Bewertungen (0–5; -1 erklärt für veraltet und entfernt). Durch eine Zusammenfassung ersetzte Einträge werden eingeklappt.",
723
857
  "settings.unavailable": "Die Konfigurationsoberfläche ist nicht verfügbar; der Gedächtnis-Abschnitt fällt auf den Standard (ein) zurück.",
724
858
  "settings.readonly": "Diese Konfiguration ist hier schreibgeschützt; Änderungen sind nicht möglich."
725
859
  },
@@ -747,10 +881,31 @@ window.__ModuleLoader__.load({
747
881
  "slots.status.done": "Completato",
748
882
  "slots.status.dropped": "Scartato",
749
883
  "slots.status.expired": "Scaduto",
884
+ "section.memoryRecall": "Richiamo della sessione",
885
+ "recall.empty": "Nessun ricordo richiamato in questa sessione",
886
+ "recall.count": "{count} inseriti · quelli sostituiti da un riassunto sono ripiegati dopo la compressione (rimossi con /clear)",
887
+ "recall.rateHint": "Seleziona un ricordo per valutarlo: 0–5 pertinenza; -1 lo dichiara obsoleto e lo ritira (ripristinabile).",
888
+ "recall.obsolete": "Obsoleto",
889
+ "recall.obsoleteConfirm": "Ritirarlo?",
890
+ "recall.obsoleted": "Dichiarato obsoleto",
891
+ "recall.rated": "Valutato {count}/5",
892
+ "recall.failed": "Valutazione non riuscita: ",
893
+ "recall.obsoleteWhy": "Dichiararlo obsoleto congeda il record (ripristinabile) e avvisa il modello al turno successivo",
894
+ "recall.strikeHint": "; una voce dichiarata obsoleta resta elencata, barrata (rimossa solo svuotando la sessione)",
895
+ "recall.supersededHidden": "{count} vecchi richiami nascosti — il testo è stato sostituito da un riassunto",
896
+ "recall.noBody": "(questo frame non contiene il testo completo di questa voce)",
897
+ "section.skillUsage": "Riferimenti alle skill",
898
+ "skillUsage.empty": "Nessuna skill caricata in questa sessione",
899
+ "skillUsage.emptyHint": "Le skill compaiono qui quando il modello ne carica una con il tool skill.",
900
+ "skillUsage.uses": "{count}x",
901
+ "skillUsage.count": "{count} caricamenti · {total} skill",
902
+ "skillUsage.latestTag": "questo giro",
750
903
  "settings.title": "Barra laterale Riepilogo",
751
904
  "settings.desc": "Interruttori delle sezioni della scheda Riepilogo nella barra laterale destra (sola lettura).",
752
905
  "settings.memorySlots": "Sezione slot di memoria",
753
906
  "settings.memorySlotsHint": "Quando attivo, la scheda Riepilogo elenca gli slot di memoria attivi di dsh-prime-memory sotto avanzamento e file prodotti (sola lettura).",
907
+ "settings.memoryRecall": "Sezione richiamo sessione",
908
+ "settings.memoryRecallHint": "Attivo: la scheda Riepilogo elenca i ricordi inseriti in questa sessione e consente di valutarli (0–5; -1 dichiara obsoleto e ritira). Le voci sostituite da un riassunto vengono ripiegate.",
754
909
  "settings.unavailable": "La superficie di configurazione non è disponibile; la sezione memoria torna al valore predefinito (attivo).",
755
910
  "settings.readonly": "Questa configurazione è in sola lettura qui; non è possibile modificarla."
756
911
  },
@@ -778,10 +933,31 @@ window.__ModuleLoader__.load({
778
933
  "slots.status.done": "Завершён",
779
934
  "slots.status.dropped": "Отброшен",
780
935
  "slots.status.expired": "Истёк",
936
+ "section.memoryRecall": "Воспоминания сессии",
937
+ "recall.empty": "В этой сессии воспоминания ещё не вызывались",
938
+ "recall.count": "Вставлено {count} · заменённые кратким изложением свёрнуты после сжатия (удаляются при /clear)",
939
+ "recall.rateHint": "Выберите запись, чтобы оценить: 0–5 релевантность; -1 объявляет её устаревшей и убирает (восстановимо).",
940
+ "recall.obsolete": "Устарело",
941
+ "recall.obsoleteConfirm": "Убрать?",
942
+ "recall.obsoleted": "Объявлено устаревшим",
943
+ "recall.rated": "Оценка {count}/5",
944
+ "recall.failed": "Ошибка оценки: ",
945
+ "recall.obsoleteWhy": "Объявить устаревшим значит снимает запись восстанавлимо и сообщает модель на следующем ходе",
946
+ "recall.strikeHint": "; помеченная как устаревшая запись остаётся в списке, зачёркнутой (удаляется только при очистке сессии)",
947
+ "recall.supersededHidden": "Скрыто старых воспоминаний: {count} — их текст заменён кратким изложением",
948
+ "recall.noBody": "В этом кадре нет полного текста этой записи",
949
+ "section.skillUsage": "Отсылки к скиллам",
950
+ "skillUsage.empty": "В этой сессии скиллы не загружались",
951
+ "skillUsage.emptyHint": "Скиллы появятся здесь, как только модель загрузит их через инструмент skill.",
952
+ "skillUsage.uses": "{count} раз",
953
+ "skillUsage.count": "Загрузок: {count} · скиллов: {total}",
954
+ "skillUsage.latestTag": "этот ход",
781
955
  "settings.title": "Боковая панель «Сводка»",
782
956
  "settings.desc": "Переключатели разделов вкладки «Сводка» на правой панели (только чтение).",
783
957
  "settings.memorySlots": "Раздел слотов памяти",
784
958
  "settings.memorySlotsHint": "Когда включено, вкладка «Сводка» показывает активные слоты памяти из dsh-prime-memory под ходом работы и созданными файлами (только чтение).",
959
+ "settings.memoryRecall": "Раздел Воспоминания сессии",
960
+ "settings.memoryRecallHint": "Включено: вкладка «Сводка» показывает вставленные в этой сессии воспоминания и позволяет оценить их (0–5; -1 объявляет устаревшим и выводит из обращения). Заменённые кратким изложением записи сворачиваются.",
785
961
  "settings.unavailable": "Панель конфигурации недоступна; раздел памяти работает по умолчанию (включён).",
786
962
  "settings.readonly": "Эта конфигурация здесь только для чтения; изменить её нельзя."
787
963
  },
@@ -809,10 +985,31 @@ window.__ModuleLoader__.load({
809
985
  "slots.status.done": "Hecha",
810
986
  "slots.status.dropped": "Descartada",
811
987
  "slots.status.expired": "Caducada",
988
+ "section.memoryRecall": "Recuerdo de la sesión",
989
+ "recall.empty": "Aún no se ha recuperado ningún recuerdo en esta sesión",
990
+ "recall.count": "{count} insertados · los reemplazados por un resumen se pliegan tras comprimir (se borran con /clear)",
991
+ "recall.rateHint": "Selecciona un recuerdo para puntuarlo: 0–5 relevancia; -1 lo declara obsoleto y lo retira (recuperable).",
992
+ "recall.obsolete": "Obsoleto",
993
+ "recall.obsoleteConfirm": "¿Retirarlo?",
994
+ "recall.obsoleted": "Declarado obsoleto",
995
+ "recall.rated": "Puntuado {count}/5",
996
+ "recall.failed": "Error al puntuar: ",
997
+ "recall.obsoleteWhy": "Declararlo obsoleto retira el registro (recuperable) y avisa al modelo en el turno siguiente",
998
+ "recall.strikeHint": "; una entrada marcada como obsoleta sigue en la lista, tachada (se retira solo al vaciar la sesión)",
999
+ "recall.supersededHidden": "{count} recuerdos antiguos ocultos — su texto fue reemplazado por un resumen",
1000
+ "recall.noBody": "(este fotograma no trae el texto completo de este registro)",
1001
+ "section.skillUsage": "Referencias de skills",
1002
+ "skillUsage.empty": "Esta sesión no ha cargado ningún skill",
1003
+ "skillUsage.emptyHint": "Los skills aparecerán aquí en cuanto el modelo cargue uno con la herramienta skill.",
1004
+ "skillUsage.uses": "{count}x",
1005
+ "skillUsage.count": "{count} cargas · {total} skills",
1006
+ "skillUsage.latestTag": "este turno",
812
1007
  "settings.title": "Barra lateral Resumen",
813
1008
  "settings.desc": "Interruptores de las secciones de la pestaña Resumen en la barra lateral derecha (solo lectura).",
814
1009
  "settings.memorySlots": "Sección de ranuras de memoria",
815
1010
  "settings.memorySlotsHint": "Al activarla, la pestaña Resumen enumera las ranuras de memoria activas de dsh-prime-memory bajo el avance y los archivos generados (solo lectura).",
1011
+ "settings.memoryRecall": "Sección de recuerdo de sesión",
1012
+ "settings.memoryRecallHint": "Activado: la pestaña Resumen lista los recuerdos inyectados en esta sesión y permite puntuarlos (0–5; -1 declara obsoleto y retira). Las entradas reemplazadas por un resumen se pliegan.",
816
1013
  "settings.unavailable": "La superficie de configuración no está disponible; la sección de memoria vuelve a su valor por defecto (activada).",
817
1014
  "settings.readonly": "Esta configuración es de solo lectura aquí; no se puede modificar."
818
1015
  }
@@ -846,24 +1043,24 @@ window.__ModuleLoader__.load({
846
1043
  *
847
1044
  * Colours come from `--dsw-alias-*` tokens only (skin contract).
848
1045
  */
849
- const SECTION_STYLE$1 = {
1046
+ const SECTION_STYLE$3 = {
850
1047
  flexShrink: 0,
851
1048
  borderBottom: "1px solid var(--dsw-alias-border-secondary)"
852
1049
  };
853
- const HEADER_STYLE$2 = {
1050
+ const HEADER_STYLE$4 = {
854
1051
  padding: "10px 12px 0",
855
1052
  color: "var(--dsw-alias-label-secondary)",
856
1053
  fontSize: 12,
857
1054
  fontWeight: 600
858
1055
  };
859
- const LIST_STYLE$1 = {
1056
+ const LIST_STYLE$3 = {
860
1057
  listStyle: "none",
861
1058
  margin: 0,
862
1059
  padding: "6px 0",
863
1060
  display: "flex",
864
1061
  flexDirection: "column"
865
1062
  };
866
- const ROW_STYLE$2 = {
1063
+ const ROW_STYLE$4 = {
867
1064
  display: "flex",
868
1065
  alignItems: "flex-start",
869
1066
  gap: 8,
@@ -907,7 +1104,7 @@ window.__ModuleLoader__.load({
907
1104
  cursor: "pointer"
908
1105
  };
909
1106
  /** The codeplan pill: same form as the status pills, tertiary emphasis. */
910
- const TAG_STYLE = {
1107
+ const TAG_STYLE$1 = {
911
1108
  flexShrink: 0,
912
1109
  color: "var(--dsw-alias-label-tertiary)",
913
1110
  border: "1px solid var(--dsw-alias-label-tertiary)",
@@ -928,12 +1125,12 @@ window.__ModuleLoader__.load({
928
1125
  const plan = splitCodeplanPath(path);
929
1126
  const title = plan === null ? path : `${t("deliverable.codeplanTag")}: ${plan.task} · ${path}`;
930
1127
  const label = basename(path);
931
- const content = plan === null ? null : (0, react.createElement)("span", { style: TAG_STYLE }, t("deliverable.codeplanTag"));
932
- if (open === void 0) return (0, react.createElement)("li", { style: ROW_STYLE$2 }, content, (0, react.createElement)("span", {
1128
+ const content = plan === null ? null : (0, react.createElement)("span", { style: TAG_STYLE$1 }, t("deliverable.codeplanTag"));
1129
+ if (open === void 0) return (0, react.createElement)("li", { style: ROW_STYLE$4 }, content, (0, react.createElement)("span", {
933
1130
  style: PATH_STYLE,
934
1131
  title
935
1132
  }, label));
936
- return (0, react.createElement)("li", { style: ROW_STYLE$2 }, content, (0, react.createElement)("button", {
1133
+ return (0, react.createElement)("li", { style: ROW_STYLE$4 }, content, (0, react.createElement)("button", {
937
1134
  style: BUTTON_STYLE,
938
1135
  title,
939
1136
  onClick: () => {
@@ -958,9 +1155,9 @@ window.__ModuleLoader__.load({
958
1155
  const latestPaths = view.latest?.paths ?? [];
959
1156
  const history = historyPaths(view.sessionPaths, latestPaths);
960
1157
  if (latestPaths.length === 0 && view.sessionTotal === 0) return (0, react.createElement)("div", {
961
- style: SECTION_STYLE$1,
1158
+ style: SECTION_STYLE$3,
962
1159
  "data-dsh-brief-sidebar": "deliverables-section"
963
- }, (0, react.createElement)("div", { style: HEADER_STYLE$2 }, t("section.deliverables")), (0, react.createElement)("div", { style: {
1160
+ }, (0, react.createElement)("div", { style: HEADER_STYLE$4 }, t("section.deliverables")), (0, react.createElement)("div", { style: {
964
1161
  margin: "0 auto",
965
1162
  padding: 24,
966
1163
  maxWidth: 460,
@@ -973,9 +1170,9 @@ window.__ModuleLoader__.load({
973
1170
  color: "var(--dsw-alias-label-secondary)"
974
1171
  } }, t("deliverables.empty")), (0, react.createElement)("div", null, t("deliverables.emptyHint"))));
975
1172
  return (0, react.createElement)("div", {
976
- style: SECTION_STYLE$1,
1173
+ style: SECTION_STYLE$3,
977
1174
  "data-dsh-brief-sidebar": "deliverables-section"
978
- }, (0, react.createElement)("div", { style: HEADER_STYLE$2 }, t("section.deliverables")), (0, react.createElement)("ul", { style: LIST_STYLE$1 }, latestPaths.map((path) => (0, react.createElement)(PathRow, {
1175
+ }, (0, react.createElement)("div", { style: HEADER_STYLE$4 }, t("section.deliverables")), (0, react.createElement)("ul", { style: LIST_STYLE$3 }, latestPaths.map((path) => (0, react.createElement)(PathRow, {
979
1176
  key: path,
980
1177
  path,
981
1178
  t,
@@ -1000,7 +1197,7 @@ window.__ModuleLoader__.load({
1000
1197
  strokeWidth: 1.5,
1001
1198
  strokeLinecap: "round",
1002
1199
  strokeLinejoin: "round"
1003
- }))), historyOpen && (0, react.createElement)("ul", { style: LIST_STYLE$1 }, history.map((path) => (0, react.createElement)(PathRow, {
1200
+ }))), historyOpen && (0, react.createElement)("ul", { style: LIST_STYLE$3 }, history.map((path) => (0, react.createElement)(PathRow, {
1004
1201
  key: path,
1005
1202
  path,
1006
1203
  t,
@@ -1008,6 +1205,182 @@ window.__ModuleLoader__.load({
1008
1205
  }))));
1009
1206
  }
1010
1207
  //#endregion
1208
+ //#region src/client/summary/skill-usage.ts
1209
+ /** Whether a value is a non-negative safe integer (the view's counter shape). */
1210
+ function isCounter$2(value) {
1211
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
1212
+ }
1213
+ /** Narrow one skill tally; `undefined` = malformed, dropped. */
1214
+ function readEntry(value) {
1215
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return void 0;
1216
+ const raw = value;
1217
+ if (typeof raw["name"] !== "string" || raw["name"].length === 0) return void 0;
1218
+ if (!isCounter$2(raw["count"]) || !isCounter$2(raw["lastTurn"])) return void 0;
1219
+ return {
1220
+ name: raw["name"],
1221
+ count: raw["count"],
1222
+ lastTurn: raw["lastTurn"]
1223
+ };
1224
+ }
1225
+ /**
1226
+ * The latest-turn bucket, or `null` when it is absent OR drifted. A drifted
1227
+ * bucket only costs the "本轮" tag, so it degrades instead of voiding the view.
1228
+ */
1229
+ function readLatest(value) {
1230
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return null;
1231
+ const raw = value;
1232
+ if (!isCounter$2(raw["turn"]) || !Array.isArray(raw["names"])) return null;
1233
+ return {
1234
+ turn: raw["turn"],
1235
+ names: raw["names"].filter((n) => typeof n === "string" && n.length > 0)
1236
+ };
1237
+ }
1238
+ /**
1239
+ * Narrow a raw projection value into a skill-usage view.
1240
+ *
1241
+ * @param value - the raw `unknown` snapshot of the `dshSummarySkillUsage` face.
1242
+ * @returns the validated view, or `undefined` when the capability is absent.
1243
+ */
1244
+ function readSkillUsage(value) {
1245
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return void 0;
1246
+ const raw = value;
1247
+ if (!Array.isArray(raw["session"])) return void 0;
1248
+ if (!isCounter$2(raw["sessionTotal"]) || !isCounter$2(raw["callTotal"])) return void 0;
1249
+ const session = [];
1250
+ for (const entry of raw["session"]) {
1251
+ const narrowed = readEntry(entry);
1252
+ if (narrowed !== void 0) session.push(narrowed);
1253
+ }
1254
+ return {
1255
+ latest: readLatest(raw["latest"]),
1256
+ session,
1257
+ sessionTotal: raw["sessionTotal"],
1258
+ callTotal: raw["callTotal"]
1259
+ };
1260
+ }
1261
+ /**
1262
+ * Fill both placeholders of a template. `expandCount` only speaks `{count}`, and
1263
+ * this section's footer carries a second number on purpose: the capped list
1264
+ * needs the truthful distinct total next to the load count.
1265
+ *
1266
+ * @param template - a locale string carrying `{count}` and/or `{total}`.
1267
+ * @param count - the `{count}` value.
1268
+ * @param total - the `{total}` value.
1269
+ * @returns the filled string.
1270
+ */
1271
+ function expandPair(template, count, total) {
1272
+ return template.replace("{count}", String(count)).replace("{total}", String(total));
1273
+ }
1274
+ //#endregion
1275
+ //#region src/client/summary/SkillUsageSection.tsx
1276
+ /**
1277
+ * The skill-usage section: the `dshSummarySkillUsage` projection drawn as one
1278
+ * part of the summary tab.
1279
+ *
1280
+ * Where `MemoryRecallSection` answers "what memories did the model see", this
1281
+ * answers "which instruction packs did it pull" — the skills this session
1282
+ * loaded through the host's `skill` tool, with how often and when.
1283
+ *
1284
+ * Like the deliverables section this is an OPTIONAL capability on both sides:
1285
+ * no projection registry (or a host half that never registered the unit) means
1286
+ * the client reads capability absence and the section hides. No settings switch
1287
+ * gates it either — the list is a read-only fact about the session, not a
1288
+ * preference, and a section that only ever shows what already happened costs
1289
+ * nothing to leave on.
1290
+ *
1291
+ * Two contracts this row honours, both learned on the recall section:
1292
+ * - **no native `title` attribute** — it paints an unskinable tooltip that
1293
+ * overlaps the plugin's own bubble; nothing here needs one;
1294
+ * - **colours from `--dsw-alias-*` tokens only** (skin contract).
1295
+ */
1296
+ const SECTION_STYLE$2 = {
1297
+ flexShrink: 0,
1298
+ borderBottom: "1px solid var(--dsw-alias-border-secondary)"
1299
+ };
1300
+ const HEADER_STYLE$3 = {
1301
+ padding: "10px 12px 0",
1302
+ color: "var(--dsw-alias-label-secondary)",
1303
+ fontSize: 12,
1304
+ fontWeight: 600
1305
+ };
1306
+ const LIST_STYLE$2 = {
1307
+ listStyle: "none",
1308
+ margin: 0,
1309
+ padding: "6px 0",
1310
+ display: "flex",
1311
+ flexDirection: "column"
1312
+ };
1313
+ const ROW_STYLE$3 = {
1314
+ display: "flex",
1315
+ alignItems: "baseline",
1316
+ gap: 8,
1317
+ padding: "6px 12px"
1318
+ };
1319
+ /** The skill name: primary-ish but never the loudest thing in the row. */
1320
+ const NAME_STYLE$1 = {
1321
+ minWidth: 0,
1322
+ overflowWrap: "anywhere",
1323
+ color: "var(--dsw-alias-label-secondary)"
1324
+ };
1325
+ /** The per-skill load tally, tertiary so the name stays the reading target. */
1326
+ const COUNT_STYLE = {
1327
+ flexShrink: 0,
1328
+ color: "var(--dsw-alias-label-tertiary)",
1329
+ fontSize: 11
1330
+ };
1331
+ /** The "本轮" pill: same form as the deliverables codeplan tag. */
1332
+ const TAG_STYLE = {
1333
+ flexShrink: 0,
1334
+ color: "var(--dsw-alias-label-tertiary)",
1335
+ border: "1px solid var(--dsw-alias-label-tertiary)",
1336
+ borderRadius: 999,
1337
+ padding: "0 6px",
1338
+ fontSize: 11,
1339
+ lineHeight: "16px",
1340
+ whiteSpace: "nowrap"
1341
+ };
1342
+ const FOOTER_STYLE$2 = {
1343
+ padding: "0 12px 10px",
1344
+ color: "var(--dsw-alias-label-tertiary)",
1345
+ fontSize: 11
1346
+ };
1347
+ const EMPTY_STYLE$2 = {
1348
+ margin: "0 auto",
1349
+ padding: 24,
1350
+ maxWidth: 460,
1351
+ textAlign: "center",
1352
+ color: "var(--dsw-alias-label-tertiary)",
1353
+ lineHeight: 1.7,
1354
+ fontSize: 12
1355
+ };
1356
+ /**
1357
+ * The skill-usage section. The subscription is unconditional (a plain listener,
1358
+ * not IO); the view decides whether anything is drawn.
1359
+ *
1360
+ * @param props - translation, client context, session scope.
1361
+ */
1362
+ function SkillUsageSection(props) {
1363
+ const { t, ctx, scope } = props;
1364
+ const view = readSkillUsage(useProjectionValue(ctx, scope?.sessionId, SKILL_USAGE_KEY));
1365
+ if (view === void 0) return null;
1366
+ if (view.session.length === 0) return (0, react.createElement)("div", {
1367
+ style: SECTION_STYLE$2,
1368
+ "data-dsh-brief-sidebar": "skill-usage-section"
1369
+ }, (0, react.createElement)("div", { style: HEADER_STYLE$3 }, t("section.skillUsage")), (0, react.createElement)("div", { style: EMPTY_STYLE$2 }, (0, react.createElement)("div", { style: {
1370
+ marginBottom: 6,
1371
+ color: "var(--dsw-alias-label-secondary)"
1372
+ } }, t("skillUsage.empty")), (0, react.createElement)("div", null, t("skillUsage.emptyHint"))));
1373
+ const latestNames = view.latest?.names ?? [];
1374
+ return (0, react.createElement)("div", {
1375
+ style: SECTION_STYLE$2,
1376
+ "data-dsh-brief-sidebar": "skill-usage-section"
1377
+ }, (0, react.createElement)("div", { style: HEADER_STYLE$3 }, t("section.skillUsage")), (0, react.createElement)("ul", { style: LIST_STYLE$2 }, view.session.map((entry) => (0, react.createElement)("li", {
1378
+ key: entry.name,
1379
+ style: ROW_STYLE$3,
1380
+ "data-dsh-brief-sidebar": "skill-usage-row"
1381
+ }, (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"))))), (0, react.createElement)("div", { style: FOOTER_STYLE$2 }, expandPair(t("skillUsage.count"), view.callTotal, view.sessionTotal)));
1382
+ }
1383
+ //#endregion
1011
1384
  //#region src/client/summary/memory-slots.ts
1012
1385
  /**
1013
1386
  * The `memorySlots` projection — reading it.
@@ -1048,7 +1421,7 @@ window.__ModuleLoader__.load({
1048
1421
  function readSlots(value) {
1049
1422
  if (value === null || typeof value !== "object" || Array.isArray(value)) return void 0;
1050
1423
  const raw = value;
1051
- if (!isCounter(raw["rev"]) || !isCounter(raw["count"]) || !isCounter(raw["openCount"])) return void 0;
1424
+ if (!isCounter$1(raw["rev"]) || !isCounter$1(raw["count"]) || !isCounter$1(raw["openCount"])) return void 0;
1052
1425
  if (!Array.isArray(raw["slots"])) return void 0;
1053
1426
  const slots = [];
1054
1427
  for (const entry of raw["slots"]) {
@@ -1063,7 +1436,7 @@ window.__ModuleLoader__.load({
1063
1436
  };
1064
1437
  }
1065
1438
  /** Whether a value is a non-negative safe integer (the view's counter shape). */
1066
- function isCounter(value) {
1439
+ function isCounter$1(value) {
1067
1440
  return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
1068
1441
  }
1069
1442
  /** Narrow one slot entry; `undefined` = malformed, dropped. */
@@ -1116,8 +1489,62 @@ window.__ModuleLoader__.load({
1116
1489
  * explicit `false` is the only off.
1117
1490
  */
1118
1491
  function resolveShowMemorySlots(snapshot) {
1492
+ if (snapshot === void 0 || snapshot.status !== "ready") return false;
1493
+ return snapshot.value?.showMemorySlots === true;
1494
+ }
1495
+ /**
1496
+ * Resolve the session-recall switch from one snapshot (pure; testable).
1497
+ * Any cannot-answer shape falls back to {@link DEFAULT_SHOW_MEMORY_RECALL};
1498
+ * an explicit `false` is the only off.
1499
+ */
1500
+ function resolveShowMemoryRecall(snapshot) {
1119
1501
  if (snapshot === void 0 || snapshot.status !== "ready") return true;
1120
- return snapshot.value?.showMemorySlots !== false;
1502
+ return snapshot.value?.showMemoryRecall !== false;
1503
+ }
1504
+ //#endregion
1505
+ //#region src/client/summary/use-config-flag.ts
1506
+ /**
1507
+ * The shared `configForms` subscription behind this plugin's section switches.
1508
+ *
1509
+ * Both memory sections (pinned slots / session recall) gate on one boolean of
1510
+ * this plugin's entry config. The plumbing — defensive scope resolution,
1511
+ * `useSyncExternalStore`, and a stable absent-snapshot — is identical, so it
1512
+ * lives here once; each section passes its own pure resolver, which is what
1513
+ * the unit tests pin.
1514
+ *
1515
+ * A host without `configForms` (or a scope still loading) degrades to the
1516
+ * resolver's default instead of throwing inside a render.
1517
+ */
1518
+ /**
1519
+ * The snapshot served while no config scope is reachable — every resolver
1520
+ * maps it to its own default.
1521
+ */
1522
+ const ABSENT_SNAPSHOT = {
1523
+ status: "unavailable",
1524
+ value: void 0,
1525
+ revision: void 0,
1526
+ writable: false
1527
+ };
1528
+ /** A no-op unsubscribe source, kept as one identity so the absent-scope
1529
+ * subscription never thrashes (a store contract requires a disposer). */
1530
+ const NO_SUBSCRIBE$1 = () => () => {};
1531
+ /**
1532
+ * Read one config switch, live.
1533
+ *
1534
+ * @param ctx - the client root context handed to the tab body.
1535
+ * @param resolve - pure snapshot → boolean resolver (owns the default).
1536
+ * @returns the resolved switch value.
1537
+ */
1538
+ function useConfigFlag(ctx, resolve) {
1539
+ const scope = (0, react.useMemo)(() => {
1540
+ if (ctx === void 0) return void 0;
1541
+ const configForms = ctx.get("configForms");
1542
+ if (configForms === void 0 || typeof configForms.get !== "function") return void 0;
1543
+ return configForms.get("dsh-brief-sidebar");
1544
+ }, [ctx]);
1545
+ const subscribe = (0, react.useCallback)((listener) => scope?.subscribe(listener) ?? NO_SUBSCRIBE$1(), [scope]);
1546
+ const getSnapshot = (0, react.useCallback)(() => scope?.getSnapshot() ?? ABSENT_SNAPSHOT, [scope]);
1547
+ return resolve((0, react.useSyncExternalStore)(subscribe, getSnapshot, getSnapshot));
1121
1548
  }
1122
1549
  //#endregion
1123
1550
  //#region src/client/summary/MemorySlotsSection.tsx
@@ -1129,9 +1556,10 @@ window.__ModuleLoader__.load({
1129
1556
  * paints nothing here:
1130
1557
  *
1131
1558
  * 1. **The switch.** This plugin's entry config (`configForms` → entry
1132
- * `dsh-brief-sidebar`, field `showMemorySlots`, default on). When the
1133
- * config face cannot answer (service absent, scope not ready, field
1134
- * unset) the default applies — see `scope-face.ts`.
1559
+ * `dsh-brief-sidebar`, field `showMemorySlots`, default OFF — the pinned
1560
+ * list moved out of the summary pane's way in favour of the session-recall
1561
+ * section). When the config face cannot answer (service absent, scope not
1562
+ * ready, field unset) the default applies — see `scope-face.ts`.
1135
1563
  * 2. **The capability.** The `memorySlots` projection must exist for the
1136
1564
  * session; `undefined` hides the section entirely, exactly like the
1137
1565
  * optional deliverables section.
@@ -1139,35 +1567,35 @@ window.__ModuleLoader__.load({
1139
1567
  * Colours come from `--dsw-alias-*` tokens only (skin contract, enforced by
1140
1568
  * `tests/purity.spec.ts`).
1141
1569
  */
1142
- const SECTION_STYLE = {
1570
+ const SECTION_STYLE$1 = {
1143
1571
  flexShrink: 0,
1144
1572
  borderBottom: "1px solid var(--dsw-alias-border-secondary)"
1145
1573
  };
1146
- const HEADER_STYLE$1 = {
1574
+ const HEADER_STYLE$2 = {
1147
1575
  padding: "10px 12px 0",
1148
1576
  color: "var(--dsw-alias-label-secondary)",
1149
1577
  fontSize: 12,
1150
1578
  fontWeight: 600
1151
1579
  };
1152
- const LIST_STYLE = {
1580
+ const LIST_STYLE$1 = {
1153
1581
  listStyle: "none",
1154
1582
  margin: 0,
1155
1583
  padding: "6px 0",
1156
1584
  display: "flex",
1157
1585
  flexDirection: "column"
1158
1586
  };
1159
- const ROW_STYLE$1 = {
1587
+ const ROW_STYLE$2 = {
1160
1588
  display: "flex",
1161
1589
  alignItems: "flex-start",
1162
1590
  gap: 8,
1163
1591
  padding: "6px 12px"
1164
1592
  };
1165
- const TITLE_STYLE = {
1593
+ const TITLE_STYLE$1 = {
1166
1594
  minWidth: 0,
1167
1595
  overflowWrap: "anywhere",
1168
1596
  color: "var(--dsw-alias-label-secondary)"
1169
1597
  };
1170
- const META_STYLE = {
1598
+ const META_STYLE$1 = {
1171
1599
  flexShrink: 0,
1172
1600
  color: "var(--dsw-alias-label-tertiary)",
1173
1601
  fontSize: 11,
@@ -1201,7 +1629,7 @@ window.__ModuleLoader__.load({
1201
1629
  color: "var(--dsw-alias-label-tertiary)",
1202
1630
  border: "1px solid var(--dsw-alias-label-tertiary)"
1203
1631
  };
1204
- const BODY_STYLE$1 = {
1632
+ const BODY_STYLE$2 = {
1205
1633
  margin: 0,
1206
1634
  minWidth: 0,
1207
1635
  overflowWrap: "anywhere",
@@ -1210,7 +1638,7 @@ window.__ModuleLoader__.load({
1210
1638
  lineHeight: "18px"
1211
1639
  };
1212
1640
  const REFS_STYLE = {
1213
- ...BODY_STYLE$1,
1641
+ ...BODY_STYLE$2,
1214
1642
  fontSize: 11
1215
1643
  };
1216
1644
  const TOTAL_STYLE = {
@@ -1218,7 +1646,7 @@ window.__ModuleLoader__.load({
1218
1646
  color: "var(--dsw-alias-label-tertiary)",
1219
1647
  fontSize: 12
1220
1648
  };
1221
- const EMPTY_STYLE = {
1649
+ const EMPTY_STYLE$1 = {
1222
1650
  margin: "0 auto",
1223
1651
  padding: 24,
1224
1652
  maxWidth: 460,
@@ -1245,57 +1673,29 @@ window.__ModuleLoader__.load({
1245
1673
  const statusLabel = STATUS_KEY[slot.status] === void 0 ? slot.status : t(STATUS_KEY[slot.status]);
1246
1674
  const title = slot.body === void 0 ? slot.title : `${slot.title} — ${slot.body}`;
1247
1675
  const refsLine = slot.refViews !== void 0 ? slot.refViews.map((view) => view.title).join(" · ") : slot.refs !== void 0 ? slot.refs.join(" · ") : void 0;
1248
- return (0, react.createElement)("li", { style: ROW_STYLE$1 }, (0, react.createElement)("span", { style: KIND_STYLE }, slot.kind), (0, react.createElement)("div", { style: {
1676
+ return (0, react.createElement)("li", { style: ROW_STYLE$2 }, (0, react.createElement)("span", { style: KIND_STYLE }, slot.kind), (0, react.createElement)("div", { style: {
1249
1677
  minWidth: 0,
1250
1678
  flex: "1 1 auto",
1251
1679
  display: "flex",
1252
1680
  flexDirection: "column",
1253
1681
  gap: 2
1254
1682
  } }, (0, react.createElement)("span", {
1255
- style: TITLE_STYLE,
1683
+ style: TITLE_STYLE$1,
1256
1684
  title
1257
- }, slot.title), slot.body !== void 0 && (0, react.createElement)("p", { style: BODY_STYLE$1 }, truncateBody(slot.body)), refsLine !== void 0 && (0, react.createElement)("p", {
1685
+ }, slot.title), slot.body !== void 0 && (0, react.createElement)("p", { style: BODY_STYLE$2 }, truncateBody(slot.body)), refsLine !== void 0 && (0, react.createElement)("p", {
1258
1686
  style: REFS_STYLE,
1259
1687
  title: slot.refs !== void 0 ? slot.refs.join("\n") : void 0
1260
- }, 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));
1688
+ }, 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));
1261
1689
  }
1262
1690
  /**
1263
- * A no-op unsubscribe source, kept as one identity so the absent-scope
1264
- * subscription never thrashes (returns a no-op disposer, as the store
1265
- * contract requires).
1266
- */
1267
- const NO_SUBSCRIBE$1 = () => () => {};
1268
- /**
1269
- * The snapshot served while no config scope is reachable — resolves to the
1270
- * switch default (on) in `resolveShowMemorySlots`.
1271
- */
1272
- const ABSENT_SNAPSHOT = {
1273
- status: "unavailable",
1274
- value: void 0,
1275
- revision: void 0,
1276
- writable: false
1277
- };
1278
- /**
1279
- * Subscribe to this plugin's entry config and resolve the section switch.
1280
- *
1281
- * The scope is resolved defensively (a host without `configForms` degrades to
1282
- * the default instead of throwing inside a render) and subscribed with
1283
- * `useSyncExternalStore`, so flipping the switch in settings updates the open
1284
- * tab without a reload.
1691
+ * Subscribe to this plugin's entry config and resolve the slots switch.
1692
+ * Plumbing lives in `use-config-flag`; the default (off) lives in the resolver.
1285
1693
  *
1286
1694
  * @param ctx - the client root context handed to the tab body.
1287
1695
  * @returns whether the memory-slots section may render at all.
1288
1696
  */
1289
1697
  function useShowMemorySlots(ctx) {
1290
- const scope = (0, react.useMemo)(() => {
1291
- if (ctx === void 0) return void 0;
1292
- const configForms = ctx.get("configForms");
1293
- if (configForms === void 0 || typeof configForms.get !== "function") return void 0;
1294
- return configForms.get("dsh-brief-sidebar");
1295
- }, [ctx]);
1296
- const subscribe = (0, react.useCallback)((listener) => scope?.subscribe(listener) ?? NO_SUBSCRIBE$1(), [scope]);
1297
- const getSnapshot = (0, react.useCallback)(() => scope?.getSnapshot() ?? ABSENT_SNAPSHOT, [scope]);
1298
- return resolveShowMemorySlots((0, react.useSyncExternalStore)(subscribe, getSnapshot, getSnapshot));
1698
+ return useConfigFlag(ctx, resolveShowMemorySlots);
1299
1699
  }
1300
1700
  /**
1301
1701
  * The memory-slots section. Both hooks run unconditionally (a conditional
@@ -1310,29 +1710,801 @@ window.__ModuleLoader__.load({
1310
1710
  if (show === false) return null;
1311
1711
  if (view === void 0) return null;
1312
1712
  if (view.slots.length === 0) return (0, react.createElement)("div", {
1313
- style: SECTION_STYLE,
1713
+ style: SECTION_STYLE$1,
1314
1714
  "data-dsh-brief-sidebar": "memory-slots-section"
1315
- }, (0, react.createElement)("div", { style: HEADER_STYLE$1 }, t("section.memorySlots")), (0, react.createElement)("div", { style: EMPTY_STYLE }, (0, react.createElement)("div", { style: {
1715
+ }, (0, react.createElement)("div", { style: HEADER_STYLE$2 }, t("section.memorySlots")), (0, react.createElement)("div", { style: EMPTY_STYLE$1 }, (0, react.createElement)("div", { style: {
1316
1716
  marginBottom: 6,
1317
1717
  color: "var(--dsw-alias-label-secondary)"
1318
1718
  } }, t("slots.empty")), (0, react.createElement)("div", null, t("slots.emptyHint"))));
1319
1719
  return (0, react.createElement)("div", {
1320
- style: SECTION_STYLE,
1720
+ style: SECTION_STYLE$1,
1321
1721
  "data-dsh-brief-sidebar": "memory-slots-section"
1322
- }, (0, react.createElement)("div", { style: HEADER_STYLE$1 }, t("section.memorySlots")), (0, react.createElement)("ul", { style: LIST_STYLE }, view.slots.map((slot) => (0, react.createElement)(SlotRow, {
1722
+ }, (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, {
1323
1723
  key: slot.id,
1324
1724
  slot,
1325
1725
  t
1326
1726
  }))), (0, react.createElement)("div", { style: TOTAL_STYLE }, expandCount(t("slots.openCount"), view.openCount)));
1327
1727
  }
1328
1728
  //#endregion
1729
+ //#region src/client/summary/memory-recall.ts
1730
+ /**
1731
+ * The `memoryRecall` projection — reading it.
1732
+ *
1733
+ * The projection unit is registered by `dsh-prime-memory` (key `memoryRecall`,
1734
+ * wire view `{rev, sessionId, count, items[]}`): the memories this session has
1735
+ * actually injected. Unlike
1736
+ * `memorySlots` (pinned, resident conventions written by hand), this set is
1737
+ * NOT resident — it is the at-a-glance answer to "what did the model see".
1738
+ * It survives compaction on purpose: `compact` only marks the entries whose
1739
+ * text left the context as `superseded` (and `/clear` empties the set), so a
1740
+ * retired row keeps its strike-through instead of vanishing.
1741
+ *
1742
+ * This plugin does NOT depend on prime-memory: with the unit unregistered the
1743
+ * client reads `undefined` from the face and the section hides. The module is
1744
+ * deliberately PURE — no React, no context, no IO — so the narrowing is
1745
+ * testable directly, exactly like `memory-slots.ts`.
1746
+ */
1747
+ /** The projection key `dsh-prime-memory` registers (its canonical constant). */
1748
+ const MEMORY_RECALL_KEY = "memoryRecall";
1749
+ /** Whether a value is a non-negative safe integer (the view's counter shape). */
1750
+ function isCounter(value) {
1751
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
1752
+ }
1753
+ /** Narrow one item entry; `undefined` = malformed, dropped. */
1754
+ function readItem(value) {
1755
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return void 0;
1756
+ const raw = value;
1757
+ if (typeof raw["id"] !== "string" || raw["id"].length === 0) return void 0;
1758
+ if (typeof raw["type"] !== "string" || typeof raw["title"] !== "string") return void 0;
1759
+ if (typeof raw["priority"] !== "number") return void 0;
1760
+ const item = {
1761
+ id: raw["id"],
1762
+ type: raw["type"],
1763
+ title: raw["title"],
1764
+ priority: raw["priority"]
1765
+ };
1766
+ if (Array.isArray(raw["scenario"])) {
1767
+ const phrases = raw["scenario"].filter((s) => typeof s === "string" && s.length > 0);
1768
+ if (phrases.length > 0) item.scenario = phrases;
1769
+ }
1770
+ const rating = raw["rating"];
1771
+ if (typeof rating === "number" && Number.isInteger(rating) && rating >= -1 && rating <= 5) item.rating = rating;
1772
+ if (typeof raw["body"] === "string") item.body = raw["body"];
1773
+ if (raw["retired"] === true) item.retired = true;
1774
+ if (raw["superseded"] === true) item.superseded = true;
1775
+ return item;
1776
+ }
1777
+ /**
1778
+ * Narrow a raw projection value into a recall view.
1779
+ *
1780
+ * `undefined` means the capability is ABSENT (prime-memory not in the
1781
+ * composition, or no frame carried the key) or the frame drifted beyond repair;
1782
+ * a view with `items: []` IS the honest empty state (prime-memory publishes as
1783
+ * soon as the unit registers). Malformed items are dropped, never thrown on.
1784
+ *
1785
+ * @param value - the raw `unknown` snapshot of the `memoryRecall` face.
1786
+ * @returns the validated view, or `undefined` when the capability is absent.
1787
+ */
1788
+ function readRecall(value) {
1789
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return void 0;
1790
+ const raw = value;
1791
+ if (!isCounter(raw["rev"]) || !isCounter(raw["count"])) return void 0;
1792
+ if (typeof raw["sessionId"] !== "string") return void 0;
1793
+ if (!Array.isArray(raw["items"])) return void 0;
1794
+ const items = [];
1795
+ for (const entry of raw["items"]) {
1796
+ const item = readItem(entry);
1797
+ if (item !== void 0) items.push(item);
1798
+ }
1799
+ return {
1800
+ rev: raw["rev"],
1801
+ sessionId: raw["sessionId"],
1802
+ count: raw["count"],
1803
+ items
1804
+ };
1805
+ }
1806
+ //#endregion
1807
+ //#region src/client/summary/recall-feedback.ts
1808
+ /**
1809
+ * The write channel back into `dsh-prime-memory`: rating one recalled memory.
1810
+ *
1811
+ * This is the FIRST write this plugin performs, and deliberately the only one.
1812
+ * Reading goes through the host's projection face (`memoryRecall`); there is no
1813
+ * projection for writes, so the mark travels over prime-memory's own client RPC
1814
+ * route — `POST /dsh-memory/rpc/<method>`, the same envelope its panel uses.
1815
+ * That is a cross-plugin HTTP contract, so everything that makes it safe to
1816
+ * hold lives here:
1817
+ *
1818
+ * - **Optional by construction.** A composition without dsh-prime-memory, or an
1819
+ * older one without this endpoint, answers 404/405. That is reported as
1820
+ * `absent`, and the section stops offering the control instead of failing on
1821
+ * every click.
1822
+ * - **No invented semantics.** `-1` and `0..5` are prime-memory's domain; this
1823
+ * module refuses anything else locally so a bad click cannot reach the server.
1824
+ * - **Nothing is optimistic about retirement.** The caller only hides a row
1825
+ * after the server confirms `retired` — a lost request must not silently
1826
+ * remove a memory from view.
1827
+ *
1828
+ * `fetch` is injected so the parse and the status mapping are testable without
1829
+ * a browser.
1830
+ */
1831
+ /** The route prime-memory's server half registers for its client RPC surface. */
1832
+ const RECALL_FEEDBACK_ROUTE = "/dsh-memory/rpc/recall-feedback";
1833
+ /** A relevance mark (`0..5`) or `-1` = "this is obsolete", prime-memory's domain. */
1834
+ const FEEDBACK_SCORES = [
1835
+ -1,
1836
+ 0,
1837
+ 1,
1838
+ 2,
1839
+ 3,
1840
+ 4,
1841
+ 5
1842
+ ];
1843
+ /** Whether a value is a score this plugin is allowed to send. */
1844
+ function isFeedbackScore(value) {
1845
+ return typeof value === "number" && FEEDBACK_SCORES.includes(value);
1846
+ }
1847
+ /** How long to wait before giving up on one click. */
1848
+ const FEEDBACK_TIMEOUT_MS = 8e3;
1849
+ /** Whether a status code means "no such route" rather than "the call failed". */
1850
+ function isAbsentStatus(status) {
1851
+ return status === 404 || status === 405 || status === 501;
1852
+ }
1853
+ /** The host's RpcResult envelope, narrowed enough to read the two branches. */
1854
+ function parseEnvelope(value) {
1855
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return void 0;
1856
+ const raw = value;
1857
+ if (raw["ok"] === true && raw["value"] !== null && typeof raw["value"] === "object") return {
1858
+ ok: true,
1859
+ value: raw["value"]
1860
+ };
1861
+ if (raw["ok"] === false) {
1862
+ const err = raw["error"];
1863
+ const message = err !== null && typeof err === "object" ? err["message"] : void 0;
1864
+ return {
1865
+ ok: false,
1866
+ reason: typeof message === "string" && message.length > 0 ? message : "rejected"
1867
+ };
1868
+ }
1869
+ }
1870
+ function num(value) {
1871
+ return typeof value === "number" && Number.isFinite(value) ? value : 0;
1872
+ }
1873
+ /**
1874
+ * Send one rating.
1875
+ *
1876
+ * Never throws: every failure mode comes back as a discriminated outcome, so a
1877
+ * click cannot break the render.
1878
+ */
1879
+ async function sendRecallFeedback(fetchImpl, request) {
1880
+ if (typeof fetchImpl !== "function") return { status: "absent" };
1881
+ if (request.sessionId.length === 0 || request.id.length === 0) return {
1882
+ status: "rejected",
1883
+ reason: "missing-session-or-id"
1884
+ };
1885
+ if (!isFeedbackScore(request.score)) return {
1886
+ status: "rejected",
1887
+ reason: "invalid-score"
1888
+ };
1889
+ const abort = typeof AbortController === "function" ? new AbortController() : void 0;
1890
+ const timer = abort === void 0 ? void 0 : setTimeout(() => {
1891
+ abort.abort();
1892
+ }, FEEDBACK_TIMEOUT_MS);
1893
+ try {
1894
+ const response = await fetchImpl(RECALL_FEEDBACK_ROUTE, {
1895
+ method: "POST",
1896
+ headers: { "content-type": "application/json" },
1897
+ body: JSON.stringify({
1898
+ sessionId: request.sessionId,
1899
+ id: request.id,
1900
+ score: request.score
1901
+ }),
1902
+ ...abort === void 0 ? {} : { signal: abort.signal }
1903
+ });
1904
+ if (!response.ok) return isAbsentStatus(response.status) ? { status: "absent" } : {
1905
+ status: "transport",
1906
+ reason: `HTTP ${response.status}`
1907
+ };
1908
+ let envelope;
1909
+ try {
1910
+ envelope = await response.json();
1911
+ } catch {
1912
+ return {
1913
+ status: "transport",
1914
+ reason: "unparseable-response"
1915
+ };
1916
+ }
1917
+ const parsed = parseEnvelope(envelope);
1918
+ if (parsed === void 0) return {
1919
+ status: "transport",
1920
+ reason: "unexpected-envelope"
1921
+ };
1922
+ if (parsed.ok === false) return {
1923
+ status: "rejected",
1924
+ reason: parsed.reason
1925
+ };
1926
+ const value = parsed.value;
1927
+ const rating = value["rating"] !== null && typeof value["rating"] === "object" ? value["rating"] : {};
1928
+ return {
1929
+ status: "sent",
1930
+ retired: value["retired"] === true,
1931
+ announced: value["announced"] === true,
1932
+ mean: num(rating["mean"]),
1933
+ rated: num(rating["rated"])
1934
+ };
1935
+ } catch (err) {
1936
+ return {
1937
+ status: "transport",
1938
+ reason: err instanceof Error && (err.name === "AbortError" || err.name === "TimeoutError") ? "timeout" : err instanceof Error ? err.message : "network"
1939
+ };
1940
+ } finally {
1941
+ if (timer !== void 0) clearTimeout(timer);
1942
+ }
1943
+ }
1944
+ //#endregion
1945
+ //#region src/client/summary/RecallBubble.tsx
1946
+ /**
1947
+ * The hover bubble for one recalled memory — the whole body, straight off the
1948
+ * projection frame, no second round-trip.
1949
+ *
1950
+ * Two shapes are load-bearing here:
1951
+ *
1952
+ * - **Stateless.** The section owns when a bubble opens and which row it shows;
1953
+ * this only draws what it is handed. No hooks, no buttons, no timers.
1954
+ * - **`position: fixed`.** The summary tab body is `overflow: auto`, so a box
1955
+ * absolutely positioned inside a row gets clipped by the scroller's border as
1956
+ * soon as the row sits near an edge. Fixed positioning plus `bubblePos`
1957
+ * clamping keeps the bubble inside the viewport instead, and anchored to the
1958
+ * row's *left* so it never lands on the list it describes.
1959
+ *
1960
+ * `pointerEvents: 'none'` is not decoration: a bubble that captures the mouse
1961
+ * would strand the very row it hovers over and leave the section unable to
1962
+ * receive the click that selects it.
1963
+ *
1964
+ * Colours come from `--dsw-alias-*` tokens only (skin contract, asserted by
1965
+ * `tests/purity.spec.ts`). There is no shadow token in that family, so the
1966
+ * bubble separates from the list by border and background layer rather than by
1967
+ * elevation — a literal `box-shadow` colour would break the contract.
1968
+ */
1969
+ /** Bubble box size, mirrored by `bubblePos`'s defaults (keep the two in sync). */
1970
+ const BUBBLE_W = 360;
1971
+ const BUBBLE_H = 220;
1972
+ /** Viewport gutter kept on every side when clamping. */
1973
+ const EDGE = 8;
1974
+ /**
1975
+ * Clamp the bubble into the viewport.
1976
+ *
1977
+ * It opens to the **left** of the row on purpose. The sidebar is right-docked
1978
+ * and only a few hundred pixels wide, so the old "anchor + 8" placement dropped
1979
+ * the box straight onto the row it came from — covering that row's rating strip
1980
+ * (the bubble is click-transparent, so the buttons stayed clickable *under* it:
1981
+ * blind-clicking a score is not an option). Opening left puts the text over the
1982
+ * chat area and leaves the whole list readable.
1983
+ *
1984
+ * Order of preference: left of the row → right of the row (only when the row
1985
+ * sits so far left that the box would run off the window) → clamped inside the
1986
+ * viewport. Pure and total.
1987
+ *
1988
+ * @param r - the hovered row's rect (`top`/`left`/`right` are read).
1989
+ * @param vw - viewport width (`window.innerWidth`).
1990
+ * @param vh - viewport height (`window.innerHeight`).
1991
+ * @param w - bubble width.
1992
+ * @param h - bubble height.
1993
+ * @returns fixed-position coordinates for the bubble.
1994
+ */
1995
+ function bubblePos(r, vw, vh, w = BUBBLE_W, h = BUBBLE_H) {
1996
+ const right = r.right ?? r.left;
1997
+ const preferred = r.left - w - EDGE;
1998
+ const fallback = right + EDGE;
1999
+ const roomOnLeft = preferred >= EDGE;
2000
+ const roomOnRight = fallback + w <= vw - EDGE;
2001
+ return {
2002
+ left: Math.max(EDGE, Math.min(roomOnLeft ? preferred : roomOnRight ? fallback : Math.min(preferred, fallback), vw - w - EDGE)),
2003
+ top: Math.max(EDGE, Math.min(r.top, vh - h - EDGE))
2004
+ };
2005
+ }
2006
+ const BUBBLE_STYLE = {
2007
+ position: "fixed",
2008
+ pointerEvents: "none",
2009
+ zIndex: 1e3,
2010
+ maxWidth: BUBBLE_W,
2011
+ maxHeight: "45vh",
2012
+ overflow: "hidden",
2013
+ padding: "8px 10px",
2014
+ borderRadius: 6,
2015
+ background: "var(--dsw-alias-bg-layer-3)",
2016
+ border: "1px solid var(--dsw-alias-border-l2)",
2017
+ color: "var(--dsw-alias-label-primary)",
2018
+ fontSize: 12,
2019
+ lineHeight: "17px"
2020
+ };
2021
+ const TYPE_STYLE$1 = {
2022
+ display: "inline-block",
2023
+ marginBottom: 4,
2024
+ color: "var(--dsw-alias-label-tertiary)",
2025
+ border: "1px solid var(--dsw-alias-label-tertiary)",
2026
+ borderRadius: 999,
2027
+ padding: "0 6px",
2028
+ fontSize: 11,
2029
+ lineHeight: "16px"
2030
+ };
2031
+ const BODY_STYLE$1 = {
2032
+ minWidth: 0,
2033
+ overflowWrap: "anywhere",
2034
+ whiteSpace: "pre-wrap",
2035
+ color: "var(--dsw-alias-label-primary)"
2036
+ };
2037
+ const SCENARIO_STYLE$1 = {
2038
+ marginTop: 4,
2039
+ minWidth: 0,
2040
+ overflowWrap: "anywhere",
2041
+ color: "var(--dsw-alias-label-tertiary)",
2042
+ fontSize: 11,
2043
+ lineHeight: "16px"
2044
+ };
2045
+ const FOOTER_STYLE$1 = {
2046
+ marginTop: 4,
2047
+ color: "var(--dsw-alias-label-tertiary)",
2048
+ fontSize: 11,
2049
+ lineHeight: "16px"
2050
+ };
2051
+ /** A retired record stays readable in the bubble, just marked. */
2052
+ const STALE_BODY_STYLE = {
2053
+ ...BODY_STYLE$1,
2054
+ textDecoration: "line-through"
2055
+ };
2056
+ /**
2057
+ * The bubble itself.
2058
+ *
2059
+ * @param props - the item to show, the clamped coordinates, and the lookup.
2060
+ * @returns the bubble element.
2061
+ */
2062
+ function RecallBubble(props) {
2063
+ const { item, pos, t } = props;
2064
+ const scenario = item.scenario === void 0 ? void 0 : item.scenario.join(" · ");
2065
+ const stale = item.retired === true;
2066
+ const rated = item.rating !== void 0 && Number.isInteger(item.rating) && item.rating >= 0 && item.rating <= 5;
2067
+ const foot = [
2068
+ `P${item.priority}`,
2069
+ rated ? expandCount(t("recall.rated"), item.rating) : void 0,
2070
+ stale ? t("recall.obsoleted") : void 0
2071
+ ].filter((s) => s !== void 0).join(" · ");
2072
+ return (0, react.createElement)("div", {
2073
+ style: {
2074
+ ...BUBBLE_STYLE,
2075
+ top: pos.top,
2076
+ left: pos.left
2077
+ },
2078
+ "data-dsh-brief-sidebar": "recall-bubble",
2079
+ role: "tooltip"
2080
+ }, (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));
2081
+ }
2082
+ //#endregion
2083
+ //#region src/client/summary/MemoryRecallSection.tsx
2084
+ /**
2085
+ * The session-recall section: what this session actually injected, drawn as a
2086
+ * part of the summary tab — and now annotatable.
2087
+ *
2088
+ * Where `MemorySlotsSection` lists hand-pinned resident conventions (written
2089
+ * once, injected every turn), this one answers a different question: **which
2090
+ * memories did the model see this session, and which are still in context**.
2091
+ * The set is not resident — it is relevance recall. Compaction does NOT drop
2092
+ * entries: prime-memory marks the ones whose text a summary replaced as
2093
+ * `superseded`, and this section hides those (the footer keeps the count).
2094
+ * Retired rows are the exception and stay visible. Only `/clear` empties the list.
2095
+ *
2096
+ * TWO gates, both fail-open to "hidden", mirroring the slots section:
2097
+ *
2098
+ * 1. **The switch.** This plugin's entry config (`configForms` → field
2099
+ * `showMemoryRecall`, default on).
2100
+ * 2. **The capability.** The `memoryRecall` projection must exist for the
2101
+ * session; `undefined` hides the section entirely — a composition without
2102
+ * dsh-prime-memory paints nothing here.
2103
+ *
2104
+ * Rating is a THIRD gate, and it fails open the same way: the mark goes back to
2105
+ * prime-memory over its client RPC route (`recall-feedback.ts`). A composition
2106
+ * whose prime-memory predates that endpoint answers 404, and the section falls
2107
+ * back to the read-only list it used to be — no per-click error, no dead button.
2108
+ *
2109
+ * What the marks mean (prime-memory owns the semantics, this only offers them):
2110
+ * - `0..5` — how relevant this recall actually was. Aggregated; it does NOT
2111
+ * rewrite any ranking on its own.
2112
+ * - `-1` — "this memory is obsolete": prime-memory retires the record (soft
2113
+ * delete, restorable from its panel) and tells the model next turn. That is
2114
+ * why it takes a second confirming click and says so on the button.
2115
+ *
2116
+ * A retired row does NOT leave this list: the retirement is the human's verdict
2117
+ * on that memory, so the row stays, struck through and unselectable, until the
2118
+ * session is cleared. A `superseded` row (its text was replaced by a summary, so
2119
+ * it is no longer in context) IS hidden, with the count kept in the footer.
2120
+ * Hovering a row opens one bubble with the whole body (`RecallBubble`)
2121
+ * — the row itself is clamped to two lines and the sidebar is narrow.
2122
+ *
2123
+ * Colours come from `--dsw-alias-*` tokens only (skin contract, enforced by
2124
+ * `tests/purity.spec.ts`).
2125
+ */
2126
+ const SECTION_STYLE = {
2127
+ flexShrink: 0,
2128
+ borderBottom: "1px solid var(--dsw-alias-border-secondary)"
2129
+ };
2130
+ const HEADER_STYLE$1 = {
2131
+ padding: "10px 12px 0",
2132
+ color: "var(--dsw-alias-label-secondary)",
2133
+ fontSize: 12,
2134
+ fontWeight: 600
2135
+ };
2136
+ const HINT_STYLE$1 = {
2137
+ padding: "2px 12px 0",
2138
+ color: "var(--dsw-alias-label-tertiary)",
2139
+ fontSize: 11,
2140
+ lineHeight: "15px"
2141
+ };
2142
+ const LIST_STYLE = {
2143
+ listStyle: "none",
2144
+ margin: 0,
2145
+ padding: "6px 0",
2146
+ display: "flex",
2147
+ flexDirection: "column"
2148
+ };
2149
+ const ROW_STYLE$1 = {
2150
+ display: "flex",
2151
+ alignItems: "flex-start",
2152
+ gap: 8,
2153
+ padding: "6px 12px"
2154
+ };
2155
+ /** Selected row: one hairline of accent, no colour literal (skin contract). */
2156
+ const ROW_SELECTED_STYLE = {
2157
+ ...ROW_STYLE$1,
2158
+ background: "var(--dsw-alias-fill-quaternary)"
2159
+ };
2160
+ /** The type pill: same form as the slot kind pill, tertiary emphasis. */
2161
+ const TYPE_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 TITLE_STYLE = {
2172
+ minWidth: 0,
2173
+ color: "var(--dsw-alias-label-secondary)",
2174
+ display: "-webkit-box",
2175
+ WebkitLineClamp: 2,
2176
+ WebkitBoxOrient: "vertical",
2177
+ overflow: "hidden",
2178
+ fontSize: 12,
2179
+ lineHeight: "17px"
2180
+ };
2181
+ /**
2182
+ * Struck-through text for a retired row. Colour drops to tertiary because a
2183
+ * dead entry must not compete with a live one for the eye.
2184
+ */
2185
+ const STRIKE_STYLE = {
2186
+ textDecoration: "line-through",
2187
+ color: "var(--dsw-alias-label-tertiary)"
2188
+ };
2189
+ const SCENARIO_STYLE = {
2190
+ margin: 0,
2191
+ minWidth: 0,
2192
+ overflowWrap: "anywhere",
2193
+ color: "var(--dsw-alias-label-tertiary)",
2194
+ fontSize: 11,
2195
+ lineHeight: "16px"
2196
+ };
2197
+ const META_STYLE = {
2198
+ flexShrink: 0,
2199
+ color: "var(--dsw-alias-label-tertiary)",
2200
+ fontSize: 11,
2201
+ lineHeight: "18px",
2202
+ whiteSpace: "nowrap"
2203
+ };
2204
+ const FOOTER_STYLE = {
2205
+ padding: "6px 12px 10px",
2206
+ color: "var(--dsw-alias-label-tertiary)",
2207
+ fontSize: 12
2208
+ };
2209
+ const EMPTY_STYLE = {
2210
+ margin: "0 auto",
2211
+ padding: 24,
2212
+ maxWidth: 460,
2213
+ textAlign: "center",
2214
+ color: "var(--dsw-alias-label-tertiary)",
2215
+ lineHeight: 1.7,
2216
+ fontSize: 12
2217
+ };
2218
+ const STRIP_STYLE = {
2219
+ display: "flex",
2220
+ flexWrap: "wrap",
2221
+ gap: 4,
2222
+ marginTop: 4
2223
+ };
2224
+ const SCORE_BUTTON_STYLE = {
2225
+ minWidth: 24,
2226
+ padding: "0 5px",
2227
+ height: 20,
2228
+ fontSize: 11,
2229
+ lineHeight: "18px",
2230
+ color: "var(--dsw-alias-label-secondary)",
2231
+ background: "none",
2232
+ border: "1px solid var(--dsw-alias-border-secondary)",
2233
+ borderRadius: 4,
2234
+ cursor: "pointer"
2235
+ };
2236
+ /** The chosen mark: same box, filled so it survives a re-render without colour literals. */
2237
+ const SCORE_ACTIVE_STYLE = {
2238
+ ...SCORE_BUTTON_STYLE,
2239
+ border: "1px solid var(--dsw-alias-label-secondary)",
2240
+ color: "var(--dsw-alias-label-primary)"
2241
+ };
2242
+ /** `-1` reads as an action, not a score: text instead of a number. */
2243
+ const OBSOLETE_BUTTON_STYLE = {
2244
+ ...SCORE_BUTTON_STYLE,
2245
+ minWidth: 0,
2246
+ whiteSpace: "nowrap"
2247
+ };
2248
+ const OBSOLETE_ARMED_STYLE = {
2249
+ ...OBSOLETE_BUTTON_STYLE,
2250
+ border: "1px solid var(--dsw-alias-border-color-danger)",
2251
+ color: "var(--dsw-alias-label-primary)",
2252
+ fontWeight: 600
2253
+ };
2254
+ const MARK_STYLE = {
2255
+ marginTop: 2,
2256
+ color: "var(--dsw-alias-label-tertiary)",
2257
+ fontSize: 11,
2258
+ lineHeight: "15px"
2259
+ };
2260
+ const ERROR_STYLE = {
2261
+ marginTop: 2,
2262
+ color: "var(--dsw-alias-label-secondary)",
2263
+ fontSize: 11,
2264
+ lineHeight: "15px",
2265
+ overflowWrap: "anywhere"
2266
+ };
2267
+ /**
2268
+ * The rating strip of one selected row: `-1` (two-step) plus `0..5`.
2269
+ *
2270
+ * Exported for tests — the section itself only mounts it for the selected row,
2271
+ * and a static render never has one selected.
2272
+ *
2273
+ * **Every click here must stop propagating.** The strip lives INSIDE the row,
2274
+ * and the row's own `onClick` toggles the selection — so an un-stopped click
2275
+ * deselects the row, this strip unmounts mid-gesture, and the `-1` button's
2276
+ * armed "确认退场?" state is rendered on a node that is already gone. That is
2277
+ * the whole "按了没反应、也没有删除线" report: the request was never sent, the
2278
+ * confirmation was never shown, the control simply vanished on the first press.
2279
+ * @param props - labels, current mark, and the click handlers.
2280
+ */
2281
+ function RatingStrip(props) {
2282
+ const { current, pending, armed } = props;
2283
+ const swallow = (e) => {
2284
+ e.stopPropagation();
2285
+ };
2286
+ return (0, react.createElement)("div", {
2287
+ style: STRIP_STYLE,
2288
+ onClick: swallow,
2289
+ "data-dsh-brief-sidebar": "recall-rating-strip"
2290
+ }, (0, react.createElement)("button", {
2291
+ type: "button",
2292
+ style: armed ? OBSOLETE_ARMED_STYLE : OBSOLETE_BUTTON_STYLE,
2293
+ disabled: pending,
2294
+ title: armed ? props.confirmLabel : props.whyLabel,
2295
+ onClick: (e) => {
2296
+ e.stopPropagation();
2297
+ props.onScore(-1);
2298
+ }
2299
+ }, armed ? props.confirmLabel : props.obsoleteLabel), ...FEEDBACK_SCORES.filter((s) => s >= 0).map((score) => (0, react.createElement)("button", {
2300
+ key: score,
2301
+ type: "button",
2302
+ style: current === score ? SCORE_ACTIVE_STYLE : SCORE_BUTTON_STYLE,
2303
+ disabled: pending,
2304
+ "aria-pressed": current === score,
2305
+ onClick: (e) => {
2306
+ e.stopPropagation();
2307
+ props.onScore(score);
2308
+ }
2309
+ }, String(score))));
2310
+ }
2311
+ /**
2312
+ * One recalled memory: type pill, first two lines of the body, its own
2313
+ * priority, and — once selected and still alive — the rating strip. Hovering
2314
+ * opens the bubble with the whole text; a retired row is struck through and
2315
+ * cannot be selected (there is nothing left to mark on a record prime-memory
2316
+ * has already retired).
2317
+ */
2318
+ function RecallRow(props) {
2319
+ const { item, selected, stale, mark, error, t } = props;
2320
+ const scenario = item.scenario === void 0 ? void 0 : item.scenario.join(" · ");
2321
+ const markText = mark === void 0 ? void 0 : mark === -1 ? t("recall.obsoleted") : expandCount(t("recall.rated"), mark);
2322
+ const staleText = stale && mark !== -1 ? t("recall.obsoleted") : void 0;
2323
+ const selectable = props.rating && !stale;
2324
+ const titleStyle = stale ? {
2325
+ ...TITLE_STYLE,
2326
+ ...STRIKE_STYLE
2327
+ } : TITLE_STYLE;
2328
+ return (0, react.createElement)("li", {
2329
+ style: selected && props.rating ? ROW_SELECTED_STYLE : ROW_STYLE$1,
2330
+ onClick: selectable ? props.onSelect : void 0,
2331
+ onMouseEnter: (e) => {
2332
+ const el = e.currentTarget;
2333
+ if (el instanceof HTMLElement) props.onEnter(el);
2334
+ },
2335
+ onMouseLeave: () => {
2336
+ props.onLeave();
2337
+ }
2338
+ }, (0, react.createElement)("span", { style: TYPE_STYLE }, item.type), (0, react.createElement)("div", { style: {
2339
+ minWidth: 0,
2340
+ flex: "1 1 auto",
2341
+ display: "flex",
2342
+ flexDirection: "column",
2343
+ gap: 2
2344
+ } }, (0, react.createElement)("span", { style: titleStyle }, item.title), scenario !== void 0 && (0, react.createElement)("p", { style: stale ? {
2345
+ ...SCENARIO_STYLE,
2346
+ ...STRIKE_STYLE
2347
+ } : SCENARIO_STYLE }, scenario), staleText !== void 0 && (0, react.createElement)("span", { style: MARK_STYLE }, staleText), markText !== void 0 && (0, react.createElement)("span", { style: MARK_STYLE }, markText), error !== void 0 && (0, react.createElement)("span", { style: ERROR_STYLE }, `${t("recall.failed")}${error}`), selected && !stale && props.rating && (0, react.createElement)(RatingStrip, {
2348
+ current: mark,
2349
+ pending: props.pending,
2350
+ armed: props.armed,
2351
+ obsoleteLabel: t("recall.obsolete"),
2352
+ confirmLabel: t("recall.obsoleteConfirm"),
2353
+ whyLabel: t("recall.obsoleteWhy"),
2354
+ onScore: props.onScore
2355
+ })), (0, react.createElement)("span", { style: META_STYLE }, `P${item.priority}`));
2356
+ }
2357
+ /** How long the `-1` button stays armed before reverting to its plain label. */
2358
+ const ARM_WINDOW_MS = 5e3;
2359
+ /** Delay before a hover opens its bubble (ms) — swiping past rows must not paint. */
2360
+ const HOVER_DELAY_MS = 150;
2361
+ /**
2362
+ * The session-recall section. All hooks run unconditionally (a conditional hook
2363
+ * order would break the moment the switch flips); the gates only decide whether
2364
+ * the resolved view paints and whether rows are clickable.
2365
+ * @param props - translation, client context, session scope, transport.
2366
+ */
2367
+ function MemoryRecallSection(props) {
2368
+ const { t, ctx, scope } = props;
2369
+ const [selectedId, setSelectedId] = (0, react.useState)(void 0);
2370
+ const [marks, setMarks] = (0, react.useState)({});
2371
+ const [staleIds, setStaleIds] = (0, react.useState)([]);
2372
+ const [hover, setHover] = (0, react.useState)(void 0);
2373
+ const pendingHover = (0, react.useRef)(null);
2374
+ const [pendingId, setPendingId] = (0, react.useState)(void 0);
2375
+ const [errors, setErrors] = (0, react.useState)({});
2376
+ const [armed, setArmed] = (0, react.useState)(false);
2377
+ const [writeAbsent, setWriteAbsent] = (0, react.useState)(false);
2378
+ const show = useConfigFlag(ctx, resolveShowMemoryRecall);
2379
+ const view = readRecall(useProjectionValue(ctx, scope?.sessionId, MEMORY_RECALL_KEY));
2380
+ const sessionId = scope?.sessionId ?? "";
2381
+ const rating = writeAbsent === false && sessionId.length > 0;
2382
+ const rate = (0, react.useCallback)((id, score) => {
2383
+ if (score === -1 && armed === false) {
2384
+ setArmed(true);
2385
+ setTimeout(() => {
2386
+ setArmed(false);
2387
+ }, ARM_WINDOW_MS);
2388
+ return;
2389
+ }
2390
+ if (pendingId !== void 0) return;
2391
+ setArmed(false);
2392
+ setPendingId(id);
2393
+ sendRecallFeedback(props.fetchImpl ?? (typeof fetch === "function" ? fetch : void 0), {
2394
+ sessionId,
2395
+ id,
2396
+ score
2397
+ }).then((outcome) => {
2398
+ setPendingId(void 0);
2399
+ if (outcome.status === "sent") {
2400
+ setMarks((current) => ({
2401
+ ...current,
2402
+ [id]: score
2403
+ }));
2404
+ setErrors((current) => {
2405
+ const next = { ...current };
2406
+ delete next[id];
2407
+ return next;
2408
+ });
2409
+ if (score === -1) setStaleIds((current) => current.includes(id) ? current : [...current, id]);
2410
+ setHover(void 0);
2411
+ if (score !== -1) setSelectedId(void 0);
2412
+ } else if (outcome.status === "absent") {
2413
+ setWriteAbsent(true);
2414
+ setSelectedId(void 0);
2415
+ } else setErrors((current) => ({
2416
+ ...current,
2417
+ [id]: outcome.reason
2418
+ }));
2419
+ });
2420
+ }, [
2421
+ armed,
2422
+ pendingId,
2423
+ props.fetchImpl,
2424
+ sessionId
2425
+ ]);
2426
+ const onSelect = (0, react.useCallback)((id) => {
2427
+ setArmed(false);
2428
+ setSelectedId((current) => current === id ? void 0 : id);
2429
+ }, []);
2430
+ const onEnter = (0, react.useCallback)((id, el) => {
2431
+ if (pendingHover.current !== null) clearTimeout(pendingHover.current);
2432
+ const rect = el.getBoundingClientRect();
2433
+ pendingHover.current = setTimeout(() => {
2434
+ pendingHover.current = null;
2435
+ setHover({
2436
+ id,
2437
+ top: rect.top,
2438
+ left: rect.left,
2439
+ right: rect.right
2440
+ });
2441
+ }, HOVER_DELAY_MS);
2442
+ }, []);
2443
+ const onLeave = (0, react.useCallback)(() => {
2444
+ if (pendingHover.current !== null) {
2445
+ clearTimeout(pendingHover.current);
2446
+ pendingHover.current = null;
2447
+ }
2448
+ setHover(void 0);
2449
+ }, []);
2450
+ (0, react.useEffect)(() => () => {
2451
+ if (pendingHover.current !== null) clearTimeout(pendingHover.current);
2452
+ }, []);
2453
+ if (show === false) return null;
2454
+ if (view === void 0) return null;
2455
+ const isStale = (item) => item.retired === true || staleIds.includes(item.id);
2456
+ const rows = view.items.filter((item) => item.superseded !== true || item.retired === true);
2457
+ const hidden = view.items.length - rows.length;
2458
+ if (rows.length === 0) return (0, react.createElement)("div", {
2459
+ style: SECTION_STYLE,
2460
+ "data-dsh-brief-sidebar": "memory-recall-section"
2461
+ }, (0, react.createElement)("div", { style: HEADER_STYLE$1 }, t("section.memoryRecall")), (0, react.createElement)("div", {
2462
+ style: EMPTY_STYLE,
2463
+ ...hidden > 0 ? { "data-dsh-brief-sidebar": "recall-superseded-note" } : {}
2464
+ }, hidden > 0 ? expandCount(t("recall.supersededHidden"), hidden) : t("recall.empty")));
2465
+ return (0, react.createElement)("div", {
2466
+ style: SECTION_STYLE,
2467
+ "data-dsh-brief-sidebar": "memory-recall-section"
2468
+ }, (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")}`), (0, react.createElement)("ul", { style: LIST_STYLE }, rows.map((item) => (0, react.createElement)(RecallRow, {
2469
+ key: item.id,
2470
+ item,
2471
+ selected: selectedId === item.id,
2472
+ stale: isStale(item),
2473
+ pending: pendingId === item.id,
2474
+ armed: armed && selectedId === item.id,
2475
+ mark: marks[item.id] ?? item.rating,
2476
+ error: errors[item.id],
2477
+ rating,
2478
+ t,
2479
+ onSelect: () => {
2480
+ onSelect(item.id);
2481
+ },
2482
+ onScore: (score) => {
2483
+ rate(item.id, score);
2484
+ },
2485
+ onEnter: (el) => {
2486
+ onEnter(item.id, el);
2487
+ },
2488
+ onLeave
2489
+ }))), (0, react.createElement)("div", { style: FOOTER_STYLE }, expandCount(t("recall.count"), view.count), hidden > 0 && (0, react.createElement)("div", { "data-dsh-brief-sidebar": "recall-superseded-note" }, expandCount(t("recall.supersededHidden"), hidden))), hover !== void 0 && typeof window !== "undefined" && (() => {
2490
+ const item = view.items.find((i) => i.id === hover.id);
2491
+ if (item === void 0) return null;
2492
+ return (0, react.createElement)(RecallBubble, {
2493
+ item,
2494
+ pos: bubblePos(hover, window.innerWidth, window.innerHeight),
2495
+ t
2496
+ });
2497
+ })());
2498
+ }
2499
+ //#endregion
1329
2500
  //#region src/client/SummaryTab.tsx
1330
2501
  /**
1331
2502
  * The summary tab body: the session's at-a-glance state, composed of sections.
1332
2503
  *
1333
2504
  * Section order: 进展 (the todos board) → 产物 (produced files, codeplan
1334
- * artifacts marked inline) → 记忆 (the active memory slots of the session,
1335
- * served by the `memorySlots` projection dsh-prime-memory registers).
2505
+ * artifacts marked inline) → 技能引用 (which skills this session loaded) →
2506
+ * 本会话召回 (what prime-memory injected into this session and compaction has
2507
+ * not overwritten) → 记忆槽位 (hand-pinned resident conventions, off by default).
1336
2508
  * Each section owns its own data chain and its own gating contract; this
1337
2509
  * component owns only the shell.
1338
2510
  *
@@ -1381,6 +2553,16 @@ window.__ModuleLoader__.load({
1381
2553
  t,
1382
2554
  ctx,
1383
2555
  scope
2556
+ }), (0, react.createElement)(SkillUsageSection, {
2557
+ key: "skill-usage",
2558
+ t,
2559
+ ctx,
2560
+ scope
2561
+ }), (0, react.createElement)(MemoryRecallSection, {
2562
+ key: "memory-recall",
2563
+ t,
2564
+ ctx,
2565
+ scope
1384
2566
  }), (0, react.createElement)(MemorySlotsSection, {
1385
2567
  key: "memory-slots",
1386
2568
  t,
@@ -1628,6 +2810,25 @@ window.__ModuleLoader__.load({
1628
2810
  cursor: "pointer"
1629
2811
  };
1630
2812
  /**
2813
+ * One switch row: label + hint on the left, checkbox on the right. Extracted
2814
+ * because the card now binds two sections (session recall / pinned slots).
2815
+ */
2816
+ function FlagRow(props) {
2817
+ return (0, react.createElement)("div", { style: ROW_STYLE }, (0, react.createElement)("div", { style: ROW_TEXT_STYLE }, (0, react.createElement)("label", {
2818
+ htmlFor: props.id,
2819
+ style: LABEL_STYLE
2820
+ }, props.label), (0, react.createElement)("p", { style: HINT_STYLE }, props.hint)), (0, react.createElement)("input", {
2821
+ id: props.id,
2822
+ type: "checkbox",
2823
+ checked: props.checked,
2824
+ disabled: props.disabled,
2825
+ style: SWITCH_STYLE,
2826
+ onChange: (event) => {
2827
+ props.onToggle(event.currentTarget.checked);
2828
+ }
2829
+ }));
2830
+ }
2831
+ /**
1631
2832
  * The settings card body. `scope` is optional on purpose: the inject factory
1632
2833
  * always provides it, but a host whose `configForms` face degrades must not
1633
2834
  * crash the settings page — the card renders the unavailable note and keeps
@@ -1639,7 +2840,8 @@ window.__ModuleLoader__.load({
1639
2840
  const snapshot = (0, react.useSyncExternalStore)((0, react.useCallback)((listener) => scope?.subscribe(listener) ?? NO_SUBSCRIBE(), [scope]), (0, react.useCallback)(() => scope?.getSnapshot() ?? ABSENT, [scope]), (0, react.useCallback)(() => scope?.getSnapshot() ?? ABSENT, [scope]));
1640
2841
  const unavailable = scope === void 0 || snapshot.status === "unavailable";
1641
2842
  const readonly = unavailable || !snapshot.writable;
1642
- const checked = snapshot.value?.showMemorySlots !== false;
2843
+ const slotsChecked = resolveShowMemorySlots(snapshot);
2844
+ const recallChecked = resolveShowMemoryRecall(snapshot);
1643
2845
  return (0, react.createElement)("div", {
1644
2846
  style: CARD_STYLE,
1645
2847
  "data-dsh-brief-sidebar": "settings-card"
@@ -1663,17 +2865,29 @@ window.__ModuleLoader__.load({
1663
2865
  strokeWidth: 1.5,
1664
2866
  strokeLinecap: "round",
1665
2867
  strokeLinejoin: "round"
1666
- }))), open && (0, react.createElement)("div", { style: BODY_STYLE }, unavailable ? (0, react.createElement)("p", { style: HINT_STYLE }, t("settings.unavailable")) : (0, react.createElement)("div", { style: ROW_STYLE }, (0, react.createElement)("div", { style: ROW_TEXT_STYLE }, (0, react.createElement)("label", {
1667
- htmlFor: "dsh-brief-sidebar-show-memory-slots",
1668
- style: LABEL_STYLE
1669
- }, t("settings.memorySlots")), (0, react.createElement)("p", { style: HINT_STYLE }, t("settings.memorySlotsHint"))), (0, react.createElement)("input", {
2868
+ }))), open && (0, react.createElement)("div", { style: BODY_STYLE }, unavailable ? (0, react.createElement)("p", { style: HINT_STYLE }, t("settings.unavailable")) : (0, react.createElement)("div", { style: {
2869
+ display: "flex",
2870
+ flexDirection: "column",
2871
+ gap: 12
2872
+ } }, (0, react.createElement)(FlagRow, {
2873
+ key: "recall",
2874
+ id: "dsh-brief-sidebar-show-memory-recall",
2875
+ label: t("settings.memoryRecall"),
2876
+ hint: t("settings.memoryRecallHint"),
2877
+ checked: recallChecked,
2878
+ disabled: readonly,
2879
+ onToggle: (next) => {
2880
+ scope?.set("showMemoryRecall", next);
2881
+ }
2882
+ }), (0, react.createElement)(FlagRow, {
2883
+ key: "slots",
1670
2884
  id: "dsh-brief-sidebar-show-memory-slots",
1671
- type: "checkbox",
1672
- checked,
2885
+ label: t("settings.memorySlots"),
2886
+ hint: t("settings.memorySlotsHint"),
2887
+ checked: slotsChecked,
1673
2888
  disabled: readonly,
1674
- style: SWITCH_STYLE,
1675
- onChange: (event) => {
1676
- scope?.set("showMemorySlots", event.currentTarget.checked);
2889
+ onToggle: (next) => {
2890
+ scope?.set("showMemorySlots", next);
1677
2891
  }
1678
2892
  })), !unavailable && !snapshot.writable && (0, react.createElement)("p", { style: HINT_STYLE }, t("settings.readonly"))));
1679
2893
  }