prts-terrarchive 0.2.0 → 0.2.2

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 (56) hide show
  1. package/README.en.md +84 -18
  2. package/README.md +64 -15
  3. package/THIRD_PARTY_NOTICES.md +10 -0
  4. package/bin/build-runtime.mjs +32 -0
  5. package/bin/check-runtime.mjs +56 -0
  6. package/bin/install.js +24 -5
  7. package/contracts/corpus_i18n_v1.schema.json +10 -10
  8. package/cordis.patch.yml +4 -4
  9. package/docs/web-access.md +24 -0
  10. package/lib/client.js +84 -23
  11. package/lib/rhine/rhine.css +1 -1
  12. package/lib/rhine/rhine.js +208 -208
  13. package/lib/runtime/licenses/js-yaml-MIT.txt +21 -0
  14. package/lib/runtime/licenses/zod-MIT.txt +21 -0
  15. package/lib/runtime/versions.json +4 -0
  16. package/lib/runtime/yaml.js +3062 -0
  17. package/lib/runtime/zod.js +14356 -0
  18. package/lib/skins/common.css +59 -0
  19. package/lib/skins/endfield-aic.css +22 -9
  20. package/lib/skins/prts-agent.css +15 -5
  21. package/lib/skins/rhine-lab.css +9 -0
  22. package/package.json +15 -8
  23. package/presets/composition.js +53 -0
  24. package/presets/definition.js +2 -2
  25. package/presets/prts/agent.cordis.yml +1 -1
  26. package/presets/prts/preset.yml +1 -1
  27. package/presets/register.js +23 -10
  28. package/resources/runtime/yaml.js +1 -0
  29. package/resources/runtime/zod.js +1 -0
  30. package/skills/prts-composition/SKILL.md +60 -0
  31. package/skills/prts-investigation/SKILL.md +19 -102
  32. package/skills/prts-investigation/references/board-guide.md +131 -0
  33. package/skills/prts-retrieval/SKILL.md +5 -3
  34. package/src/composition-skill.js +19 -0
  35. package/src/i18n.js +11 -17
  36. package/src/index.js +63 -81
  37. package/src/investigation-review.js +14 -6
  38. package/src/investigation-store.js +97 -25
  39. package/src/investigation-tools.js +67 -30
  40. package/src/investigation-validation.js +33 -0
  41. package/src/manual-search.js +1 -1
  42. package/src/skill.js +25 -16
  43. package/src/skin-capabilities.js +28 -0
  44. package/src/tools.js +13 -0
  45. package/src/web-guidance.js +44 -0
  46. package/ui/rhine/archive-rack.ts +10 -8
  47. package/ui/rhine/catalogue.ts +16 -4
  48. package/ui/rhine/evidence-board-panel.ts +15 -0
  49. package/ui/rhine/investigation-board.css +3 -0
  50. package/ui/rhine/investigation-board.ts +71 -24
  51. package/ui/rhine/investigation-history.ts +15 -0
  52. package/ui/rhine/reading-room.css +14 -3
  53. package/ui/rhine/scene.ts +44 -24
  54. package/ui/rhine/tool-activity.ts +13 -6
  55. package/ui/rhine/types.ts +6 -0
  56. package/ui/rhine/workbench.ts +82 -40
@@ -102,6 +102,7 @@ export function mountRhineWorkbench(host: HTMLElement, options: RhineOptions): R
102
102
  const quality = { ...WORKBENCH_QUALITY };
103
103
  const navigationLimiter = new ArchiveNavigationLimiter();
104
104
  let manualSources: ArchiveSource[] = [];
105
+ let restoredSources: ArchiveSource[] = [];
105
106
  let extracts: Extract[] = [];
106
107
  let sources: ArchiveSource[] = [];
107
108
  let discoveredSources: ArchiveSource[] = [];
@@ -190,6 +191,7 @@ export function mountRhineWorkbench(host: HTMLElement, options: RhineOptions): R
190
191
  let navigationHint: { axis: 'row' | 'lane'; direction: number } | undefined;
191
192
  let hostState: RhineHostState | undefined = options.host?.getState();
192
193
  let hostActionBusy = false;
194
+ let hostCancelBusy = false;
193
195
  let hostActionError = '';
194
196
  let hostRetry: (() => Promise<void>) | undefined;
195
197
  let hostTrigger: HTMLElement | null = null;
@@ -273,8 +275,8 @@ export function mountRhineWorkbench(host: HTMLElement, options: RhineOptions): R
273
275
  </section>
274
276
  <section class="rhine-desk-panel" aria-label="本次调查档案架" hidden>
275
277
  <div class="tiny-label">EVIDENCE RACK / <span class="rhine-desk-count">00</span></div><h2>调查档案架</h2><p class="rhine-rack-intro">选择标题定位档案,抽取后阅读原文。</p><label class="rhine-shelf-search"><span aria-hidden="true">⌕</span><input class="rhine-shelf-filter" type="search" placeholder="在全部档案中筛选标题、内容…" aria-label="筛选全部档案"/><button type="button" class="rhine-shelf-clear" hidden aria-label="清除档案筛选">×</button></label><div class="rhine-rack-range" role="status"></div>
276
- <div class="rhine-shelf-current"><span class="rhine-shelf-number">S-000</span><h3 class="rhine-shelf-title">等待第一份资料</h3><div class="rhine-shelf-meta"></div><p class="rhine-shelf-excerpt">Agent 实际读过或你主动收藏的资料,留在这里供回查。</p><label class="rhine-evidence-target"><span>证据盒归属</span><select aria-label="证据盒归属" disabled><option>正在读取调查板…</option></select></label><div class="rhine-priority-actions"><button type="button" class="rhine-shelf-open">抽取阅读 <span>↗</span></button><button type="button" class="rhine-shelf-stage">+ 放入重点证据盒</button></div></div>
277
- <div class="rhine-shelf-navigation"><button type="button" class="rhine-shelf-prev" aria-label="翻阅上一份资料">←</button><div class="rhine-desk-list" role="group" aria-label="档案标题选择列表"></div><button type="button" class="rhine-shelf-next" aria-label="翻阅下一份资料">→</button></div><div class="rhine-desk-pagination"></div><button type="button" class="rhine-rack-directory">全部资料目录 <span>↗</span></button>
278
+ <div class="rhine-shelf-current"><span class="rhine-shelf-number">S-000</span><h3 class="rhine-shelf-title">等待第一份资料</h3><div class="rhine-shelf-meta"></div><p class="rhine-shelf-excerpt">已交付给 Agent 的资料与手动收藏,留在这里供回查;入架不代表已读原文。</p><label class="rhine-evidence-target"><span>证据盒归属</span><select aria-label="证据盒归属" disabled><option>正在读取调查板…</option></select></label><div class="rhine-priority-actions"><button type="button" class="rhine-shelf-open">抽取阅读 <span>↗</span></button><button type="button" class="rhine-shelf-stage">+ 放入重点证据盒</button></div></div>
279
+ <div class="rhine-shelf-navigation"><button type="button" class="rhine-shelf-prev" aria-label="翻阅上一份资料">←</button><div class="rhine-desk-list" role="group" aria-label="档案标题选择列表"></div><button type="button" class="rhine-shelf-next" aria-label="翻阅下一份资料">→</button></div><div class="rhine-desk-pagination"></div><button type="button" class="rhine-rack-directory">档案架目录 <span>↗</span></button>
278
280
  <div class="rhine-rack-legend"><span data-state="found">查得</span><span data-state="read">Agent 已读</span><span data-state="cited">已引用</span><span data-state="saved">手动收藏</span></div><button type="button" class="rhine-shelf-report" hidden>调查报告 <span>↗</span></button>
279
281
  </section>
280
282
  <button type="button" class="rhine-location-button"><span class="rhine-location-title">调查档案架 <b>→</b></span><span class="rhine-location-count">00</span></button>
@@ -286,7 +288,7 @@ export function mountRhineWorkbench(host: HTMLElement, options: RhineOptions): R
286
288
  <form class="rhine-agent-form" aria-label="向 Agent 提问">
287
289
  <div class="rhine-agent-heading"><label for="rhine-agent-question">ASK THE ARCHIVE</label><div class="rhine-agent-settings" aria-label="调查配置" hidden><button type="button" class="rhine-session-mode" aria-haspopup="dialog"><small>MODE</small><span>选择模式</span><i aria-hidden="true">⌄</i></button><button type="button" class="rhine-session-model" aria-haspopup="dialog"><small>MODEL</small><span>选择模型</span><i aria-hidden="true">⌄</i></button></div><span class="rhine-agent-availability">AGENT / READY</span></div>
288
290
  <div class="rhine-agent-field"><span aria-hidden="true">⌕</span><input id="rhine-agent-question" type="text" autocomplete="off" placeholder="输入问题,交给 Agent 调查…" maxlength="3000" aria-label="调查问题"/><button type="button" class="rhine-agent-cancel" hidden aria-label="停止当前任务">■ 停止</button><button type="submit" class="rhine-agent-submit" aria-label="提交调查问题">SEND <span>↗</span></button></div>
289
- <div class="rhine-agent-board-context" title="查看或切换调查板不会改变 Agent 的工作板"></div><div class="rhine-agent-footer"><span class="rhine-agent-hint">候选资料在检索阵列;已读与收藏资料入架。</span><button type="button" class="rhine-manual-search">检索资料 <span>→</span></button></div>
291
+ <div class="rhine-agent-board-context" title="查看或切换调查板不会改变 Agent 的工作板"></div><div class="rhine-agent-footer"><span class="rhine-agent-hint">资料交付给 Agent 后入架,已读与引用状态分别保留。</span><button type="button" class="rhine-manual-search">检索资料 <span>→</span></button></div>
290
292
  <button type="button" class="rhine-session-alert" hidden aria-haspopup="dialog"><span>会话设置暂不可用</span><b>查看详情 ↗</b></button>
291
293
  </form>
292
294
  <section class="rhine-activity" aria-label="Agent 检索状态" hidden><span class="rhine-status-dot"></span><span class="rhine-status-title" role="status"></span><span class="rhine-activity-query"></span><span class="rhine-activity-detail"></span></section>
@@ -300,8 +302,8 @@ export function mountRhineWorkbench(host: HTMLElement, options: RhineOptions): R
300
302
  </section>
301
303
  <section class="rhine-basket modal-backdrop" aria-label="收藏与摘录" hidden><div class="terminal-modal rhine-basket-modal"><div class="modal-top"><span>RHINE LAB / RESEARCH EXTRACTS</span><button type="button" class="rhine-close-basket" aria-label="关闭收藏">CLOSE <span>×</span></button></div><h2>RESEARCH EXTRACTS<small>收藏与摘录 <span class="rhine-extract-count">00</span></small></h2><p class="rhine-panel-description">选取原文,保存线索和来源。摘录随你提交的调查问题一起发送。</p><div class="rhine-basket-list"></div><button type="button" class="rhine-send-extracts solid-button">将摘录交给 Agent ↗</button></div></section>
302
304
  <section class="rhine-log modal-backdrop" aria-label="Agent 调查记录" hidden><div class="terminal-modal rhine-log-modal"><div class="modal-top"><span>RHINE LAB / INVESTIGATION RECORDS</span><button type="button" class="rhine-close-log" aria-label="关闭调查记录">CLOSE <span>×</span></button></div><h2>RESEARCH LOG<small>调查记录</small></h2><div class="rhine-log-status"></div><div class="rhine-log-content"><div class="rhine-log-sources"></div><div class="rhine-log-records-heading tiny-label">TOOL RECEIPTS / 工具调用记录</div><div class="rhine-log-records"></div><div class="rhine-log-answer"></div></div></div></section>
303
- <section class="rhine-rack-index modal-backdrop" aria-label="调查资料目录" hidden><div class="terminal-modal rhine-rack-index-modal"><div class="modal-top"><span>RHINE LAB / EVIDENCE INDEX</span><button type="button" class="rhine-close-rack-index" aria-label="关闭调查资料目录">CLOSE <span>×</span></button></div><h2>档案架目录<small>Agent 已读与手动收藏</small></h2><label class="search-field"><span>⌕</span><input class="rhine-rack-filter" type="search" autocomplete="off" placeholder="筛选资料标题、来源或内容" aria-label="筛选档案架资料"/></label><label class="rhine-evidence-target"><span>证据盒归属</span><select aria-label="证据盒归属" disabled><option>正在读取调查板…</option></select></label><div class="rhine-rack-index-list"></div><div class="rhine-rack-index-count" role="status"></div></div></section>
304
- <section class="rhine-report modal-backdrop" aria-label="Agent 调查报告" hidden><div class="terminal-modal rhine-report-modal"><div class="modal-top"><span>RHINE LAB / RESEARCH REPORT</span><button type="button" class="rhine-close-report" aria-label="关闭调查报告">返回调查 <span>×</span></button></div><header class="rhine-report-header"><div class="rhine-report-kicker tiny-label">RESEARCH REPORT</div><h2 class="rhine-report-title">调查报告</h2><div class="rhine-report-status" role="status"></div></header><div class="rhine-report-layout"><aside class="rhine-report-outline" aria-label="报告目录"><div class="tiny-label">CONTENTS / 目录</div><nav class="rhine-report-toc"></nav></aside><article class="rhine-report-body" tabindex="0" aria-label="Agent 回答"></article><aside class="rhine-report-references" aria-label="报告引用"><div class="rhine-report-sources"></div></aside></div><footer class="rhine-report-footer"><button type="button" class="rhine-report-rack">翻阅调查资料 <span>↗</span></button><span class="rhine-report-position"></span></footer></div></section>
305
+ <section class="rhine-rack-index modal-backdrop" aria-label="调查资料目录" hidden><div class="terminal-modal rhine-rack-index-modal"><div class="modal-top"><span>RHINE LAB / EVIDENCE INDEX</span><button type="button" class="rhine-close-rack-index" aria-label="关闭调查资料目录">CLOSE <span>×</span></button></div><h2>档案架目录<small>Agent 上下文资料与手动收藏</small></h2><label class="search-field"><span>⌕</span><input class="rhine-rack-filter" type="search" autocomplete="off" placeholder="筛选资料标题、来源或内容" aria-label="筛选档案架资料"/></label><label class="rhine-evidence-target"><span>证据盒归属</span><select aria-label="证据盒归属" disabled><option>正在读取调查板…</option></select></label><div class="rhine-rack-index-list"></div><div class="rhine-rack-index-count" role="status"></div></div></section>
306
+ <section class="rhine-report modal-backdrop" aria-label="本轮答复" hidden><div class="terminal-modal rhine-report-modal"><div class="modal-top"><span>RHINE LAB / CONVERSATION</span><button type="button" class="rhine-close-report" aria-label="关闭本轮答复">返回调查 <span>×</span></button></div><header class="rhine-report-header"><div class="rhine-report-kicker tiny-label">RESEARCH REPORT</div><h2 class="rhine-report-title">调查报告</h2><div class="rhine-report-status" role="status"></div></header><div class="rhine-report-layout"><aside class="rhine-report-outline" aria-label="报告目录"><div class="tiny-label">CONTENTS / 目录</div><nav class="rhine-report-toc"></nav></aside><article class="rhine-report-body" tabindex="0" aria-label="Agent 回答"></article><aside class="rhine-report-references" aria-label="报告引用"><div class="rhine-report-sources"></div></aside></div><footer class="rhine-report-footer"><button type="button" class="rhine-report-rack">翻阅调查资料 <span>↗</span></button><span class="rhine-report-position"></span></footer></div></section>
305
307
  <section class="rhine-session-panel modal-backdrop" role="dialog" aria-modal="true" aria-labelledby="rhine-session-heading" hidden><div class="terminal-modal rhine-session-modal"><div class="modal-top"><span>RHINE LAB / SESSION CONTROL</span><button type="button" class="rhine-close-session" aria-label="关闭对话管理">CLOSE <span>×</span></button></div><h2 id="rhine-session-heading">RESEARCH SESSIONS<small>对话与模式</small></h2><p class="rhine-session-workspace"></p><div class="rhine-session-error" role="alert" hidden><p></p><button type="button" class="rhine-session-retry">重试 ↗</button></div><div class="rhine-session-content"><section class="rhine-session-config" aria-label="对话设置"><div class="rhine-session-mode-field"><label for="rhine-mode-select">AGENT MODE <span>调查模式</span></label><div class="rhine-session-select-row"><select id="rhine-mode-select" aria-describedby="rhine-mode-description rhine-mode-hint"></select><button type="button" class="rhine-mode-apply">使用此模式</button></div><p id="rhine-mode-description"></p><p id="rhine-mode-hint"></p></div><div class="rhine-session-model-field" hidden><label for="rhine-model-select">MODEL <span>当前对话模型</span></label><div class="rhine-session-select-row"><select id="rhine-model-select"></select><button type="button" class="rhine-model-apply">切换模型</button></div></div></section><section class="rhine-session-history-section" aria-label="历史会话"><div class="rhine-session-list-heading"><span>SESSION ARCHIVE / 历史会话</span><button type="button" class="rhine-session-refresh">刷新 ↻</button></div><div class="rhine-session-list" tabindex="-1"></div></section></div><div class="rhine-session-modal-footer"><span class="rhine-session-notice" role="status"></span><button type="button" class="rhine-session-create solid-button">+ 新建对话</button></div></div></section>
306
308
  <button type="button" class="rhine-completion-notice" aria-label="调查已完成,阅读报告" hidden><span>✓ 调查完成</span><strong>阅读调查报告</strong><b aria-hidden="true">↗</b></button><div class="rhine-completion-announcement" role="status" aria-live="polite"></div><div class="rhine-toast toast" role="status" hidden></div>
307
309
  </div>
@@ -358,18 +360,18 @@ export function mountRhineWorkbench(host: HTMLElement, options: RhineOptions): R
358
360
  let investigationReading: ReadingObject | null = null;
359
361
  let investigationReturn = { fullscreen: false, kind: 'report' as 'report' | 'clue' | 'inbox' };
360
362
  let evidenceTarget = 'auto';
361
- let investigationContext: InvestigationContext = { sessionId: snapshot.sessionId, boards: [], selectedId: '', workingId: '', ready: false };
363
+ let investigationContext: InvestigationContext = { sessionId: snapshot.sessionId, boards: [], selectedId: '', workingId: '', ready: false, version:0, readOnly:false, reportAvailable:false };
362
364
  let contextSignature = '';
363
365
  let investigation: ReturnType<typeof mountInvestigationBoard> | undefined;
364
366
  const archiveRack = createArchiveRack({ api: options.api,
365
- changed(value) { manualSources=value;persist();mergeSources();updateReaderSave(); }, failed: toast });
367
+ changed(value) { restoredSources=value;manualSources=value.filter(source=>source.saved);persist();mergeSources();updateReaderSave(); }, failed: toast });
366
368
  const boardPanel = mountEvidenceBoardPanel(stage, {
367
369
  getCardBounds: id => scene?.getBoardCardBounds(id) || null,
368
370
  managed: true,
371
+ isReadOnly: () => investigation?.getContext().readOnly === true,
369
372
  sessionId: snapshot.sessionId,
370
373
  onChange(cards) {
371
374
  investigation?.userChange(cards);
372
- $('.rhine-board-count').textContent = String(cards.length).padStart(2, '0');
373
375
  },
374
376
  onSelect(id) { scene?.selectBoardCard(id); },
375
377
  onToolChange(tool) {
@@ -393,9 +395,9 @@ export function mountRhineWorkbench(host: HTMLElement, options: RhineOptions): R
393
395
  investigation = mountInvestigationBoard(stage, {
394
396
  sessionId: snapshot.sessionId, api: options.api, panel: boardPanel,
395
397
  onRackRevision(revision) { void archiveRack.refresh(revision).catch(()=>{}); },
396
- onCards(cards) { scene?.setBoardCards(cards); $('.rhine-board-count').textContent = String(Math.max(0, cards.length - 1)).padStart(2, '0'); },
398
+ onCards(cards,total) { scene?.setBoardCards(cards); $('.rhine-board-count').textContent = String(total).padStart(2, '0'); },
397
399
  openSource(source,inbox=false) { investigation?.holdReadingContext();investigationReturn={fullscreen:boardFullscreen,kind:inbox?'inbox':investigationReading?.kind||'report'};openSource(source,'investigation'); }, notify: toast,
398
- onContext(value){investigationContext=value;renderInvestigationContext();},
400
+ onContext(value){investigationContext=value;renderInvestigationContext();renderSavedReportEntry();},
399
401
  onInbox(value,open){if(open)boardPanel.closeTools();scene?.setEvidenceInbox(value,open);},
400
402
  askInbox:options.agentAvailable===false?undefined:async(boardId,title)=>{await options.askAgent(`请继续整理调查板「${title}」(board_id: ${boardId})的重点证据盒。先读取这块板和 inbox,resume 同一调查;核对待整理资料,尤其是我手动选入的材料,将有依据的内容整理为线索。不要仅为清空盒子而生成线索。`);},
401
403
  onReading(value) { investigationReading=value;syncReadingObject(); },
@@ -458,6 +460,7 @@ export function mountRhineWorkbench(host: HTMLElement, options: RhineOptions): R
458
460
  const sourceMotion = new ReadCardMotion();
459
461
  let agentReadFocus: ReturnType<typeof latestReadFocus>;
460
462
  let followAgent = true;
463
+ let followPausedByUser = false;
461
464
  const unseenReads = new Set<string>();
462
465
  const observedReads = new Set<string>();
463
466
  let activityTurn = '';
@@ -734,13 +737,14 @@ export function mountRhineWorkbench(host: HTMLElement, options: RhineOptions): R
734
737
  updateArchiveSelection(next?.id || null, next ? archiveLane(next) : archiveLaneIndex, false);
735
738
  }
736
739
  function renderFollowMode() {
737
- $('.rhine-follow-status').textContent = followAgent ? '跟随 Agent 查阅' : `手动浏览${unseenReads.size ? ` · 新查阅 ${unseenReads.size} 次` : ' · 自动跟随已暂停'}`;
740
+ $('.rhine-follow-status').textContent = followAgent ? '跟随 Agent 查阅' : `${followPausedByUser ? '已暂停跟随' : '手动浏览 · 下次查阅时跟随'}${unseenReads.size ? ` · 新查阅 ${unseenReads.size} 次` : ''}`;
738
741
  $('.rhine-follow-toggle').textContent = followAgent ? '暂停跟随' : '恢复跟随 ↗';
739
742
  $('.rhine-follow-toggle').setAttribute('aria-pressed', String(followAgent));
740
743
  }
741
- function setFollowAgent(value: boolean) {
744
+ function setFollowAgent(value: boolean, explicit = false) {
745
+ if (explicit || value) followPausedByUser = !value;
742
746
  followAgent = value; scene?.setFollowAgent(value);
743
- if (value) { unseenReads.clear(); if (agentReadFocus && !selectedSource) scene?.selectArchiveSource(agentReadFocus.source.id, false); }
747
+ if (value) { unseenReads.clear(); if (agentReadFocus && !selectedSource && location === 'archive') scene?.selectArchiveSource(agentReadFocus.source.id, false); }
744
748
  renderFollowMode();
745
749
  }
746
750
  function selectArchiveSource(id: string) {
@@ -833,6 +837,7 @@ export function mountRhineWorkbench(host: HTMLElement, options: RhineOptions): R
833
837
  }
834
838
  function storageKey() { return `prts-rhine-desk:v1:${encodeURIComponent(snapshot.sessionId || 'default')}`; }
835
839
  function loadSaved() {
840
+ restoredSources = [];
836
841
  manualSources = [];
837
842
  extracts = [];
838
843
  try {
@@ -898,7 +903,7 @@ export function mountRhineWorkbench(host: HTMLElement, options: RhineOptions): R
898
903
  const hostSessionNodes = new Map<string, HTMLButtonElement>();
899
904
  function renderHostControls() { return performancePanel.capture.measureWork('host-controls', renderHostControlsBody); }
900
905
  function renderHostControlsBody() {
901
- const signature = JSON.stringify([hostState, hostActionBusy, hostActionError, extracting]);
906
+ const signature = JSON.stringify([hostState, hostActionBusy, hostCancelBusy, hostActionError, extracting]);
902
907
  if (signature === hostPaintSignature) return;
903
908
  hostPaintSignature = signature;
904
909
  $('.rhine-session-controls').hidden = !options.host;
@@ -974,9 +979,17 @@ export function mountRhineWorkbench(host: HTMLElement, options: RhineOptions): R
974
979
  for (const [id, entry] of hostSessionNodes) entry.disabled = busy || id === hostState.sessionId;
975
980
  const cancel = $<HTMLButtonElement>('.rhine-agent-cancel');
976
981
  cancel.hidden = !hostState.running;
977
- cancel.disabled = busy;
982
+ cancel.disabled = hostCancelBusy || Boolean(hostState.cancelling);
983
+ cancel.textContent = cancel.disabled ? '正在停止…' : '停止';
978
984
  renderHostChoice();
979
985
  }
986
+ async function cancelAgent() {
987
+ if (!options.host || hostCancelBusy || hostState?.cancelling || !hostState?.running) return;
988
+ hostCancelBusy = true; renderAgentControls();
989
+ try { await options.host.cancel(); }
990
+ catch (error) { if (!disposed) toast(plainError(error)); }
991
+ finally { if (!disposed) { hostCancelBusy = false; hostState = options.host.getState(); renderAgentControls(); } }
992
+ }
980
993
  async function runHostAction(action: () => Promise<void>, closeOnSuccess = false) {
981
994
  if (!options.host || hostBlocked() || extracting) return;
982
995
  hostActionBusy = true; hostActionError = ''; hostRetry = undefined;
@@ -1027,6 +1040,23 @@ export function mountRhineWorkbench(host: HTMLElement, options: RhineOptions): R
1027
1040
  scene?.setLocation(next);
1028
1041
  syncMode(); renderStatus();
1029
1042
  }
1043
+ function renderSavedReportEntry() {
1044
+ const saved=investigationContext.report, completed=investigationComplete();
1045
+ const reportEntry=$('.rhine-report-open');
1046
+ const visible=!!saved||investigationActivity(snapshot).showReport;
1047
+ reportEntry.hidden=!visible;$('.rhine-shelf-report').hidden=!visible;
1048
+ reportEntry.dataset.state=completed?'complete':snapshot.running?'running':snapshot.outcome==='interrupted'||snapshot.error?'error':'returned';
1049
+ $('.rhine-report-open strong').textContent=saved?'阅读调查报告':'阅读本轮答复';
1050
+ if(saved){
1051
+ $('.rhine-report-open-kicker').textContent=`SAVED REPORT / V${saved.version}`;
1052
+ $('.rhine-report-open-meta').textContent=`${saved.title} · ${saved.clues} 条依据 · ${saved.sources} 份来源`;
1053
+ }else{
1054
+ const received=mergeSourcesInOrder([],currentInvestigationSources());
1055
+ const citations=(snapshot.reportSources||received.filter(source=>source.state==='cited')).length;
1056
+ $('.rhine-report-open-kicker').textContent=completed?'✓ 调查完成 / REPORT READY':snapshot.running?'LIVE REPORT / 正在生成':snapshot.outcome==='interrupted'?'调查已停止 / 已返回内容':'RESEARCH REPORT / 已返回回答';
1057
+ $('.rhine-report-open-meta').textContent=`本轮查得 ${received.length} 份资料 · ${citations} 份引用${completed?' · 随时核对原文':''}`;
1058
+ }
1059
+ }
1030
1060
  function renderInvestigationContext() {
1031
1061
  const context=investigationContext;
1032
1062
  const signature=JSON.stringify([context,evidenceTarget]);
@@ -1056,7 +1086,9 @@ export function mountRhineWorkbench(host: HTMLElement, options: RhineOptions): R
1056
1086
  function mergeSourcesBody(animate: boolean) {
1057
1087
  uiUpdates.sourceMerges++;
1058
1088
  activitySources = undefined;
1059
- discoveredSources = mergeSourcesInOrder(discoveredSources, [...snapshot.sources || [], ...manualSources]);
1089
+ // Snapshot sources are actual tool deliveries, also on older hosts without the flag.
1090
+ const delivered = (snapshot.sources || []).map(source => source.agentReceived ? source : { ...source, agentReceived: true });
1091
+ discoveredSources = mergeSourcesInOrder(discoveredSources, [...delivered, ...restoredSources, ...manualSources]);
1060
1092
  sources = mergeSourcesInOrder(sources, discoveredSources.filter(belongsOnShelf));
1061
1093
  const signature = JSON.stringify(sources);
1062
1094
  scene?.setInvestigation(snapshot);
@@ -1142,15 +1174,15 @@ export function mountRhineWorkbench(host: HTMLElement, options: RhineOptions): R
1142
1174
  deskPage = Math.min(deskPage, Math.max(0, Math.ceil(sources.length / SHELF_PAGE_SIZE) - 1));
1143
1175
  const pageSources = sources.slice(deskPage * SHELF_PAGE_SIZE, (deskPage + 1) * SHELF_PAGE_SIZE);
1144
1176
  const visible = query ? sources.filter(source => matchesSource(source, query)) : pageSources;
1145
- $('.rhine-rack-range').textContent = query ? `全部档案 · 匹配 ${visible.length} / ${sources.length} 份` : sources.length ? `本架 ${deskPage * SHELF_PAGE_SIZE + 1}–${Math.min(sources.length, (deskPage + 1) * SHELF_PAGE_SIZE)} / 共 ${sources.length} 份资料` : 'Agent 读过或手动收藏后入架';
1177
+ $('.rhine-rack-range').textContent = query ? `全部档案 · 匹配 ${visible.length} / ${sources.length} 份` : sources.length ? `本架 ${deskPage * SHELF_PAGE_SIZE + 1}–${Math.min(sources.length, (deskPage + 1) * SHELF_PAGE_SIZE)} / 共 ${sources.length} 份资料` : '资料交付给 Agent 或手动收藏后入架';
1146
1178
  $('.rhine-shelf-clear').hidden = !query;
1147
- $('.rhine-rack-directory').firstChild!.textContent = `全部资料目录 · ${sources.length} `;
1179
+ $('.rhine-rack-directory').firstChild!.textContent = `档案架目录 · ${sources.length} `;
1148
1180
  if (!pageSources.some(source => source.id === shelfSelected)) shelfSelected = pageSources[0]?.id || null;
1149
1181
  const focused = sources.find(source => source.id === shelfSelected);
1150
1182
  $('.rhine-shelf-number').textContent = focused ? `已选 S-${String(sourceNumber(focused)).padStart(3, '0')} / ${shelfSlot(sources.indexOf(focused)).level === 0 ? '下层' : '上层'}` : 'NO SOURCES YET';
1151
1183
  $('.rhine-shelf-title').textContent = focused?.title || '留一个位置,给下一条线索。';
1152
1184
  $('.rhine-shelf-meta').textContent = focused ? `${kind(focused)} / ${origin(focused)} / ${state(focused)}${focused.saved ? ' / 已收藏' : ''}` : '';
1153
- $('.rhine-shelf-excerpt').textContent = focused?.excerpt || (focused ? '抽取档案,查看完整原文和 Agent 实际读过的段落。' : 'Agent 实际读过或你主动收藏的资料,留在这里供回查。');
1185
+ $('.rhine-shelf-excerpt').textContent = focused?.excerpt || (focused ? '抽取档案,查看完整原文和 Agent 实际读过的段落。' : '已交付给 Agent 的资料与手动收藏,留在这里供回查;入架不代表已读原文。');
1154
1186
  $<HTMLButtonElement>('.rhine-shelf-open').disabled = !focused;
1155
1187
  $<HTMLButtonElement>('.rhine-shelf-stage').disabled = !focused;
1156
1188
  for (const selector of ['.rhine-shelf-prev', '.rhine-shelf-next']) $<HTMLButtonElement>(selector).disabled = visible.length < 2;
@@ -1172,7 +1204,7 @@ export function mountRhineWorkbench(host: HTMLElement, options: RhineOptions): R
1172
1204
  slot.setAttribute('aria-pressed', String(source.id === shelfSelected));
1173
1205
  if (list.children[position] !== slot) list.insertBefore(slot, list.children[position] || null);
1174
1206
  });
1175
- if (!visible.length) list.append(node('p', 'rhine-empty', query ? '没有匹配的档案,试试更短的标题或清除筛选。' : 'Agent 实际读过或你主动收藏的资料,会按入架顺序显示在这里。'));
1207
+ if (!visible.length) list.append(node('p', 'rhine-empty', query ? '没有匹配的档案,试试更短的标题或清除筛选。' : '已交付给 Agent 或你主动收藏的资料,会按入架顺序显示在这里。'));
1176
1208
  list.dataset.selected = shelfSelected || '';
1177
1209
  if (previousSelected !== list.dataset.selected && location === 'desk') {
1178
1210
  const chosen = shelfSelected && shelfChoiceNodes.get(shelfSelected);
@@ -1211,7 +1243,7 @@ export function mountRhineWorkbench(host: HTMLElement, options: RhineOptions): R
1211
1243
  row.querySelector('span')!.textContent = `${kind(source)} · ${origin(source)} · ${state(source)} ↗`;
1212
1244
  if (list.children[index] !== row) list.insertBefore(row, list.children[index] || null);
1213
1245
  });
1214
- if (!matches.length) list.append(node('p', 'rhine-empty', sources.length ? '没有匹配的资料,换一个关键词试试。' : 'Agent 已读或你手动收藏的资料,会在这里按入架顺序显示。'));
1246
+ if (!matches.length) list.append(node('p', 'rhine-empty', sources.length ? '没有匹配的资料,换一个关键词试试。' : '已交付给 Agent 或你手动收藏的资料,会在这里按入架顺序显示。'));
1215
1247
  $('.rhine-rack-index-count').textContent = `显示 ${matches.length} / ${sources.length} 份资料 · 点击标题打开原文`;
1216
1248
  }
1217
1249
  function openRackIndex() {
@@ -1546,7 +1578,7 @@ export function mountRhineWorkbench(host: HTMLElement, options: RhineOptions): R
1546
1578
  basket.hidden = true;
1547
1579
  resultsPanel.hidden = true;
1548
1580
  syncModalState();
1549
- toast('调查已交给 Agent,候选资料进入检索阵列,Agent 读过后入架。');
1581
+ toast('调查已交给 Agent,检索返回的资料会进入档案架。');
1550
1582
  } catch (error) { if (!disposed && snapshot.sessionId === submissionSession) toast(`未能开始调查:${plainError(error)}`); }
1551
1583
  finally { if (!disposed && snapshot.sessionId === submissionSession) { extracting = false; $<HTMLButtonElement>('.rhine-agent-submit').disabled = false; $<HTMLButtonElement>('.rhine-ask-agent').disabled = false; renderBasket(); renderStatus(); } }
1552
1584
  }
@@ -1677,7 +1709,7 @@ export function mountRhineWorkbench(host: HTMLElement, options: RhineOptions): R
1677
1709
  reportText = ''; reportSourceSignature = ''; reportTocSignature = '';
1678
1710
  }
1679
1711
  reportMeasure('report-header', () => {
1680
- reportTextContent($('.rhine-report-title'), investigationTitle() || '调查报告');
1712
+ reportTextContent($('.rhine-report-title'), '本轮答复');
1681
1713
  const completed = investigationComplete();
1682
1714
  reportTextContent($('.rhine-report-kicker'), completed ? 'INVESTIGATION COMPLETE / 调查完成' : snapshot.running ? 'LIVE REPORT / 正在生成' : 'RESEARCH RECORD / 调查记录');
1683
1715
  reportTextContent($('.rhine-report-status'), snapshot.error || (snapshot.running ? '内容随回答更新。你可以先阅读,或点右侧引用核对原文。' : completed ? '本轮调查已完成 · 点击目录跳转章节,点击引用查阅原文。' : snapshot.outcome === 'interrupted' ? '本轮调查已停止 · 以下保留停止前已返回的内容。' : '已返回的回答 · 来源可在右侧逐份查阅。'));
@@ -1742,7 +1774,20 @@ export function mountRhineWorkbench(host: HTMLElement, options: RhineOptions): R
1742
1774
  if (report.hidden && (snapshot.running || !snapshot.answer || reportText === snapshot.answer && reportInvestigationKey === investigationKey())) return;
1743
1775
  reportRenderTimer = window.setTimeout(() => { reportRenderTimer = undefined; if (!disposed) renderReport(); }, report.hidden || snapshot.running ? 180 : 0);
1744
1776
  }
1745
- function openReport(returning = false) { return reportMeasure('report-open', () => openReportBody(returning)); }
1777
+ async function openReport(returning = false) {
1778
+ if (!returning && investigation) {
1779
+ try {
1780
+ const sessionId=snapshot.sessionId;
1781
+ const available=await investigation.prepareReport();
1782
+ if(disposed||snapshot.sessionId!==sessionId)return;
1783
+ if(available){
1784
+ reportViewedKey=investigationKey();syncCompletionNotice();
1785
+ setLocation('board');investigation.showReport();return;
1786
+ }
1787
+ } catch(error) { if(!disposed)toast(plainError(error));return; }
1788
+ }
1789
+ return reportMeasure('report-open', () => openReportBody(returning));
1790
+ }
1746
1791
  function openReportBody(returning: boolean) {
1747
1792
  if (!returning && document.activeElement instanceof HTMLElement && !report.contains(document.activeElement) && !readerReturn && !reader.contains(document.activeElement)) reportTrigger = document.activeElement;
1748
1793
  // Populate while hidden so parsing/patching cannot repeatedly flush visible report layout.
@@ -1774,14 +1819,14 @@ export function mountRhineWorkbench(host: HTMLElement, options: RhineOptions): R
1774
1819
  function syncToolActivityBody() {
1775
1820
  const turn = `${snapshot.sessionId}:${snapshot.investigationId || ''}`;
1776
1821
  if (activityTurn !== turn) {
1777
- activityTurn = turn; agentReadFocus = undefined;
1822
+ activityTurn = turn;
1778
1823
  sourceMotion.dispose();
1779
1824
  }
1780
1825
  const next = latestReadFocus(snapshot, activitySources ??= mergeSourcesInOrder(snapshot.sources || [], archiveSources));
1781
1826
  const changed = Boolean(next && next.key !== agentReadFocus?.key);
1782
1827
  for(const operation of snapshot.operations||[]){
1783
1828
  if(operation.kind!=='read')continue;
1784
- const key=`${turn}:${operation.id}`;
1829
+ const key=`${snapshot.sessionId}:${operation.id}`;
1785
1830
  if(!observedReads.has(key)&&!followAgent&&activityInitialized)unseenReads.add(key);
1786
1831
  observedReads.add(key);
1787
1832
  }
@@ -1790,6 +1835,10 @@ export function mountRhineWorkbench(host: HTMLElement, options: RhineOptions): R
1790
1835
  const operation = snapshot.operations?.find(call => call.id === agentReadFocus?.operation.id);
1791
1836
  if (operation) agentReadFocus = { ...agentReadFocus, operation };
1792
1837
  }
1838
+ // Manual navigation only holds the card until the next resolved Agent read.
1839
+ // An explicit pause remains sticky. The open reader is independent of this
1840
+ // card, so its document and scroll position never change during a new read.
1841
+ if (changed && !followAgent && !followPausedByUser) setFollowAgent(true);
1793
1842
  updateArchiveSelection(archiveSelected, archiveLaneIndex, false);
1794
1843
  renderFollowMode();
1795
1844
  if (followAgent && changed && next?.operation.state !== 'error' && activityInitialized && snapshot.running && active && !document.hidden && !selectedSource)
@@ -1856,13 +1905,6 @@ export function mountRhineWorkbench(host: HTMLElement, options: RhineOptions): R
1856
1905
  $('.rhine-found-count').textContent = String(received.length).padStart(2, '0');
1857
1906
  $('.rhine-read-count').textContent = String(reads).padStart(2, '0');
1858
1907
  $('.rhine-cited-count').textContent = String(citations).padStart(2, '0');
1859
- $('.rhine-report-open').hidden = !activity.showReport;
1860
- $('.rhine-shelf-report').hidden = !activity.showReport;
1861
- const reportEntry = $('.rhine-report-open');
1862
- reportEntry.dataset.state = completed ? 'complete' : snapshot.running ? 'running' : snapshot.outcome === 'interrupted' || snapshot.error ? 'error' : 'returned';
1863
- $('.rhine-report-open-kicker').textContent = completed ? '✓ 调查完成 / REPORT READY' : snapshot.running ? 'LIVE REPORT / 正在生成' : snapshot.outcome === 'interrupted' ? '调查已停止 / 已返回内容' : 'RESEARCH REPORT / 已返回回答';
1864
- $('.rhine-report-open strong').textContent = completed ? '阅读调查报告' : '查看已返回的回答';
1865
- $('.rhine-report-open-meta').textContent = `本轮查得 ${received.length} 份资料 · ${citations} 份引用${completed ? ' · 随时核对原文' : ''}`;
1866
1908
  if (completed && snapshot.answer && lastCompletedKey !== investigationKey()) {
1867
1909
  lastCompletedKey = investigationKey();
1868
1910
  if (hasRenderedStatus) {
@@ -1874,6 +1916,7 @@ export function mountRhineWorkbench(host: HTMLElement, options: RhineOptions): R
1874
1916
  }
1875
1917
  if (!completed) for (const selector of ['.rhine-report-open', '.rhine-completion-notice']) $(selector).classList.remove('is-new');
1876
1918
  hasRenderedStatus = true;
1919
+ renderSavedReportEntry();
1877
1920
  syncCompletionNotice();
1878
1921
  $('.rhine-activity').hidden = location !== 'desk' || !running && !snapshot.error;
1879
1922
  $('.rhine-status-title').textContent = searching ? 'RETRIEVING ARCHIVES' : snapshot.running ? 'INVESTIGATION IN PROGRESS' : 'INVESTIGATION / PAUSED';
@@ -1942,7 +1985,7 @@ export function mountRhineWorkbench(host: HTMLElement, options: RhineOptions): R
1942
1985
  evidenceTarget=select.value;renderInvestigationContext();
1943
1986
  });
1944
1987
  $('.rhine-array-stage').addEventListener('click',()=>{const source=displayedArchiveSource;if(source)pinToBoard(source);});
1945
- $('.rhine-follow-toggle').addEventListener('click',()=>{setFollowAgent(!followAgent);updateArchiveSelection(archiveSelected,archiveLaneIndex,false);});
1988
+ $('.rhine-follow-toggle').addEventListener('click',()=>{setFollowAgent(!followAgent, true);updateArchiveSelection(archiveSelected,archiveLaneIndex,false);});
1946
1989
  $('.rhine-shelf-stage').addEventListener('click',()=>{const source=sources.find(item=>item.id===shelfSelected);if(source)pinToBoard(source);});
1947
1990
  $('.rhine-shelf-open').addEventListener('click', () => { const source = sources.find(item => item.id === shelfSelected); if (source) void openSource(source); });
1948
1991
  $('.rhine-agent-form').addEventListener('submit', event => { event.preventDefault(); void askAgent(); });
@@ -1963,7 +2006,7 @@ export function mountRhineWorkbench(host: HTMLElement, options: RhineOptions): R
1963
2006
  }
1964
2007
  });
1965
2008
  $('.rhine-model-apply').addEventListener('click', () => { const model = modelSelect.value; if (options.host && hostState?.models.some(item => item.id === model)) void runHostAction(() => options.host!.selectModel(model)); });
1966
- $('.rhine-agent-cancel').addEventListener('click', () => { if (options.host && hostState?.running) void runHostAction(() => options.host!.cancel()); });
2009
+ $('.rhine-agent-cancel').addEventListener('click', () => { void cancelAgent(); });
1967
2010
  modeSelect.addEventListener('change', renderHostChoice);
1968
2011
  modelSelect.addEventListener('change', renderHostChoice);
1969
2012
  $('.rhine-nav-close').addEventListener('click', options.close);
@@ -2058,7 +2101,6 @@ export function mountRhineWorkbench(host: HTMLElement, options: RhineOptions): R
2058
2101
  syncMode();
2059
2102
  stage.classList.toggle('reduce-motion', reducedMotion.matches);
2060
2103
  root.dataset.location = location;
2061
- const initialCatalogue = search(false, false);
2062
2104
 
2063
2105
  // Keep the moment loading began as the history baseline. Fast reads that
2064
2106
  // finish while the GLB loads are new activity, not historical playback.
@@ -2086,6 +2128,7 @@ export function mountRhineWorkbench(host: HTMLElement, options: RhineOptions): R
2086
2128
  onLocationRequest: next => { setFollowAgent(false); setLocation(next); },
2087
2129
  onInboxOpen:()=>investigation?.openInbox(),
2088
2130
  onBoardSelect: (id, openEditor) => { if (!disposed) investigation?.select(id, openEditor); },
2131
+ isBoardReadOnly: () => investigation?.getContext().readOnly === true,
2089
2132
  onBoardMove: (id, x, y) => { if (!disposed) boardPanel.moveCard(id, x, y); },
2090
2133
  onBoardResize: (id, scale, x, y) => { if (!disposed) boardPanel.resizeCard(id, scale, x, y); },
2091
2134
  onBoardAnchors: frame => { if (!disposed) { boardFrame = frame; investigation?.setBoardFrame(frame); syncBoardToolbar(); } },
@@ -2100,7 +2143,6 @@ export function mountRhineWorkbench(host: HTMLElement, options: RhineOptions): R
2100
2143
  if (!value) return;
2101
2144
  if (disposed || sceneStartupCancelled) { value.dispose(); return; }
2102
2145
  scene = value;
2103
- await initialCatalogue;
2104
2146
  if (disposed || sceneStartupCancelled) return;
2105
2147
  $('.rhine-loader-message').textContent = '正在准备模型与画面…';
2106
2148
  scene.setFollowAgent(followAgent);
@@ -2151,6 +2193,7 @@ export function mountRhineWorkbench(host: HTMLElement, options: RhineOptions): R
2151
2193
  const changedSession = changes.sessionChanged;
2152
2194
  snapshot = next;
2153
2195
  if (changedSession) {
2196
+ agentReadFocus = undefined; activityInitialized = false; activityTurn = '';
2154
2197
  setBoardFullscreen(false);
2155
2198
  evidenceTarget='auto';contextSignature='';
2156
2199
  boardPanel.closeTools();
@@ -2168,7 +2211,7 @@ export function mountRhineWorkbench(host: HTMLElement, options: RhineOptions): R
2168
2211
  setArrayListExpanded(false, true);
2169
2212
  results = []; resultsPage = 0; deskPage = 0; searchCursor = undefined; searchDataVersion = undefined;
2170
2213
  sources = []; discoveredSources = []; archiveSources = []; archiveSelected = null; shelfSelected = null;
2171
- followAgent = true; unseenReads.clear(); observedReads.clear(); scene?.setFollowAgent(true);
2214
+ followAgent = true; followPausedByUser = false; unseenReads.clear(); observedReads.clear(); scene?.setFollowAgent(true);
2172
2215
  archiveLaneIndex = 0; columnMemory.fill(null); catalogueLoaded = false;
2173
2216
  sourceSignature = ''; input.value = ''; agentInput.value = ''; extracting = false;
2174
2217
  readerBody.replaceChildren(); $('.rhine-reader-title').textContent = '';
@@ -2188,7 +2231,6 @@ export function mountRhineWorkbench(host: HTMLElement, options: RhineOptions): R
2188
2231
  searchQuery = ''; searchError = ''; searchWarning = ''; manualSearch.resetMode(); searchMode = 'local';
2189
2232
  scene?.setInvestigation(snapshot); scene?.setSources([], false); scene?.setArchiveSources([]);
2190
2233
  setLocation('archive'); updateArchiveSelection(null, 0, false); renderDesk();
2191
- void search(false, false);
2192
2234
  visibleRecordCount = 20; recordNodes.clear(); $('.rhine-log-records').replaceChildren();
2193
2235
  logSourcesSignature = logRecordsSignature = ''; activitySources = undefined;
2194
2236
  loadSaved(); renderBasket();
@@ -2204,7 +2246,7 @@ export function mountRhineWorkbench(host: HTMLElement, options: RhineOptions): R
2204
2246
  } finally { finishWork?.(); }
2205
2247
  },
2206
2248
  setActive(next: boolean) { active = next; if (!next) { cancelAnimationFrame(readerPositionFrame); readerPositionFrame = 0; } edgeNavigation?.sync(); performancePanel.setActive(next); scene?.setActive(next && !viewer?.isOpen); root.hidden = !next; if (next) { scheduleReportPosition(); scheduleReaderPosition(); } else { cancelAnimationFrame(reportPositionFrame); reportPositionFrame = 0; } if (next && viewer?.isOpen && !viewerFrame) viewerFrame = requestAnimationFrame(renderViewer); else if (!next) { cancelAnimationFrame(viewerFrame); viewerFrame = 0; } },
2207
- stats() { return { uiUpdates: { ...uiUpdates }, hostBridge: options.host?.performanceStats?.() ?? null, snapshotBridge: options.snapshotDiagnostics?.performanceStats?.() ?? null, ...scene?.stats(), board: boardPanel.stats(), investigations: investigation?.stats(), boardFullscreen, navigation: navigationLimiter.stats(), quality: { ...quality }, sourceCount: sources.length, candidateCount: archiveSources.length, followAgent, unseenReads: unseenReads.size, displayedSourceId: displayedArchiveSource?.id, manualSourceCount: manualSources.length, extractCount: extracts.length, location, readerOpen: !reader.hidden, searchBusy, resultCount: results.length, reportOpen: !report.hidden, shelfSelected, deskPage, viewerOpen: Boolean(viewer?.isOpen), archiveIndex: archiveSelected }; },
2249
+ stats() { return { uiUpdates: { ...uiUpdates }, hostBridge: options.host?.performanceStats?.() ?? null, snapshotBridge: options.snapshotDiagnostics?.performanceStats?.() ?? null, ...scene?.stats(), board: boardPanel.stats(), investigations: investigation?.stats(), boardFullscreen, navigation: navigationLimiter.stats(), quality: { ...quality }, sourceCount: sources.length, candidateCount: archiveSources.length, followAgent, followPausedByUser, unseenReads: unseenReads.size, displayedSourceId: displayedArchiveSource?.id, manualSourceCount: manualSources.length, extractCount: extracts.length, location, readerOpen: !reader.hidden, searchBusy, resultCount: results.length, reportOpen: !report.hidden, shelfSelected, deskPage, viewerOpen: Boolean(viewer?.isOpen), archiveIndex: archiveSelected }; },
2208
2250
  dispose() {
2209
2251
  manualSearch.dispose();
2210
2252
  if (disposed) return;