Dataset-only tags:
{(focusedEpoch.curation?.tags||[]).map(t=>
curate({tags_remove:[t]},'focused')}>{t} )}
{!focusedEpoch.curation?.tags?.length&&
No tags }
diff --git a/workspace-app/src/epochViewerRendering.test.js b/workspace-app/src/epochViewerRendering.test.js
new file mode 100644
index 0000000..d744981
--- /dev/null
+++ b/workspace-app/src/epochViewerRendering.test.js
@@ -0,0 +1,41 @@
+import test,{before,after} from 'node:test';
+import assert from 'node:assert/strict';
+import {fileURLToPath} from 'node:url';
+import {createElement} from 'react';
+import {renderToString} from 'react-dom/server';
+import {createServer} from 'vite';
+import react from '@vitejs/plugin-react';
+
+let server;
+before(async()=>{
+ server=await createServer({root:fileURLToPath(new URL('..',import.meta.url)),configFile:false,plugins:[react()],server:{middlewareMode:true,hmr:false},appType:'custom',logLevel:'error'});
+});
+after(async()=>{await server?.close();});
+function render(Component,props){
+ const previous=console.error;
+ // Layout effects intentionally wait for the browser; SSR still exercises
+ // adapter render expressions (the null-epoch crash happened before effects).
+ console.error=(message,...args)=>{if(!String(message).startsWith('Warning: useLayoutEffect does nothing on the server'))previous(message,...args);};
+ try{return renderToString(createElement(Component,props));}finally{console.error=previous;}
+}
+test('opening a pinned inspector before any epoch is focused renders its empty state without dereferencing curation',async()=>{
+ const {default:Inspector}=await server.ssrLoadModule('/src/components/Inspector.jsx');
+ const html=render(Inspector,{protocol:{definition:{protocol_uuid:'fixture',name:'Variable History Noise'},cells:[]},filters:{},revision:0});
+ assert.match(html,/Choose an epoch/);
+ assert.match(html,/Epoch list controls/);
+ assert.doesNotMatch(html,/Dataset-only tags:/);
+ assert.doesNotMatch(html,/Include in analysis/);
+});
+test('opening Search results before any epoch is focused uses the same safe viewer empty state',async()=>{
+ const {default:MatchingEpochs}=await server.ssrLoadModule('/src/components/MatchingEpochs.jsx');
+ const html=render(MatchingEpochs,{predicate:{all:[]},splits:'date,cell',preview:{tree_revision:'fixture',catalog:{fields:[]}},viewFilters:{},onViewFilters:()=>{}});
+ assert.match(html,/Choose an epoch/);
+ assert.match(html,/Epoch list controls/);
+ assert.doesNotMatch(html,/Include in analysis/);
+});
+test('shared viewer accepts no selected epoch and no source-specific extras',async()=>{
+ const {default:EpochViewer}=await server.ssrLoadModule('/src/components/EpochViewer.jsx');
+ const html=render(EpochViewer,{layout:{sizes:{columns:'minmax(0,1fr)',metadata:300},treeOpen:false,metadataOpen:false,onResize:()=>{}},epoch:null});
+ assert.match(html,/Choose an epoch/);
+ assert.doesNotMatch(html,/Include in analysis/);
+});
From fa9eb0808aeac177a244e29dae69c67038abfc67 Mon Sep 17 00:00:00 2001
From: arthurh <49833088+maxwellsdm1867@users.noreply.github.com>
Date: Tue, 29 Sep 2026 17:01:55 -0700
Subject: [PATCH 4/8] Share recorded scientific context and remove obsolete
tree UI
---
workspace-app/src/components/EpochViewer.jsx | 3 +-
workspace-app/src/components/Inspector.jsx | 92 ++-----------------
.../src/components/ScientificContext.jsx | 35 +++++++
.../src/epochViewerRendering.test.js | 11 +++
4 files changed, 56 insertions(+), 85 deletions(-)
create mode 100644 workspace-app/src/components/ScientificContext.jsx
diff --git a/workspace-app/src/components/EpochViewer.jsx b/workspace-app/src/components/EpochViewer.jsx
index 95c0472..ee78eca 100644
--- a/workspace-app/src/components/EpochViewer.jsx
+++ b/workspace-app/src/components/EpochViewer.jsx
@@ -1,3 +1,4 @@
+import ScientificContext from './ScientificContext.jsx';
import {EpochBrowserToolbar,EpochNavigation} from './EpochBrowserChrome.jsx';
import EpochBrowserLayout from './EpochBrowserLayout.jsx';
import EpochTreePane from './EpochTreePane.jsx';
@@ -28,6 +29,6 @@ export default function EpochViewer({className='epoch-inspector-mode',ariaLabel=
:
{!layout.metadataOpen&&{tags} } }
detail={designMode?
:detail}
- metadata={metadata&&
}/>
+ metadata={metadata&&
}/> }/>
;
}
diff --git a/workspace-app/src/components/Inspector.jsx b/workspace-app/src/components/Inspector.jsx
index 75a4587..e208721 100644
--- a/workspace-app/src/components/Inspector.jsx
+++ b/workspace-app/src/components/Inspector.jsx
@@ -1,12 +1,12 @@
import EpochViewer from './EpochViewer.jsx';
import SelectionMaskDialog from './SelectionMaskDialog.jsx';
import {NavigationLoadingProvider} from './NavigationLoading.jsx';
-import {advanceEpochIntent,epochIntentAt,epochAtIntent} from '../epochNavigationIntent.js';
+import {advanceEpochIntent, epochIntentAt, epochAtIntent} from '../epochNavigationIntent.js';
import {SourceEligibilityNotice} from './Common.jsx';
-import { useCallback, useEffect, useMemo, useRef, useState } from 'react';
-import { ChevronDown, ChevronRight, GitBranch, Check, X, MessageCircle, ArrowLeft, ArrowRight, Activity, Eye, Info, PanelRightClose, PanelRightOpen, Upload, Download, FileJson } from 'lucide-react';
-import { api, useResource, useEpochResource, useEpochPrefetch, number, humanize, resolveCurationTargets } from '../api.js';
-import { Status, Metadata, Badge, Empty } from './Common.jsx';
+import {useCallback, useEffect, useRef, useState} from 'react';
+import {GitBranch, Check, X, Eye, Upload, Download, FileJson} from 'lucide-react';
+import {api, useResource, useEpochResource, useEpochPrefetch, number, humanize, resolveCurationTargets} from '../api.js';
+import {Badge} from './Common.jsx';
import EpochConnections from './EpochConnections.jsx';
import './InspectorPolish.css';
import './MatlabMaskImport.css';
@@ -15,89 +15,13 @@ import AnnotationTags from './AnnotationTags.jsx';
import TagExchangeControls from './TagExchangeControls.jsx';
import './Inspector.css';
-import {inspectionSearches,indexInspectionTree,focusAfterTreeSelection,curationMatchesCellFocus} from '../inspectionScope.js';
-import {branchLabel,branchTooltip,componentLabel,componentValue,epochLeafLabel,readableField} from '../treeBranchPresentation.js';
+import {inspectionSearches, curationMatchesCellFocus} from '../inspectionScope.js';
import {datedCellLabel} from '../recordingIdentity.js';
import {restoredEpochFocus} from '../workspaceNavigation.js';
-import {boundedTreePage,inspectionTreeFocus} from '../boundedTree.js';
import Trace from './TraceViewer.jsx';
-import {inspectorPaneSizes,epochShortcutDirection,resourceForPath} from '../inspectorInteraction.js';
+import {inspectorPaneSizes, epochShortcutDirection, resourceForPath} from '../inspectorInteraction.js';
export {Trace};
-function TreePageControls({page,onPage,kind}){
- if(page.total<=60&&!page.pinnedOutside)return null;
- return