This closes #197
diff --git a/ui-modules/app-inspector/app/components/entity-tree/entity-tree.html b/ui-modules/app-inspector/app/components/entity-tree/entity-tree.html
index 67c0ce1..b105215 100644
--- a/ui-modules/app-inspector/app/components/entity-tree/entity-tree.html
+++ b/ui-modules/app-inspector/app/components/entity-tree/entity-tree.html
@@ -21,5 +21,5 @@
 <div class="empty-tree text-muted text-center" ng-if="vm.applications.length === 0">
     <hr />
     <h4>No applications deployed yet.</h4>
-    <p>You can create one via the <a href="/brooklyn-ui-blueprint-composer" class="btn btn-primary btn-xs">blueprint composer</a> or deploy an existing <a href="/" class="btn btn-primary btn-xs">catalog item</a>.</p>
+    <p>You can create one via the Composer by clicking "+" above or deploy an existing <a href="/" class="btn btn-primary btn-xs">catalog item</a>.</p>
 </div>
diff --git a/ui-modules/app-inspector/app/views/main/main.controller.js b/ui-modules/app-inspector/app/views/main/main.controller.js
index c691a10..31502f8 100644
--- a/ui-modules/app-inspector/app/views/main/main.controller.js
+++ b/ui-modules/app-inspector/app/views/main/main.controller.js
@@ -23,17 +23,19 @@
     name: 'main',
     url: '/',
     template: template,
-    controller: ['$scope', '$q', 'brWebNotifications', mainController],
+    controller: ['$scope', '$q', 'brWebNotifications', 'brBrandInfo', mainController],
     controllerAs: 'ctrl'
 };
 
 const savedSortReverse = 'app-inspector-sort-reverse';
 
-export function mainController($scope, $q, brWebNotifications) {
+export function mainController($scope, $q, brWebNotifications, brBrandInfo) {
     $scope.$emit(HIDE_INTERSTITIAL_SPINNER_EVENT);
 
     let ctrl = this;
 
+    ctrl.composerUrl = brBrandInfo.blueprintComposerBaseUrl;
+
     ctrl.sortReverse = localStorage && localStorage.getItem(savedSortReverse) !== null ?
         JSON.parse(localStorage.getItem(savedSortReverse)) :
         true;
diff --git a/ui-modules/app-inspector/app/views/main/main.template.html b/ui-modules/app-inspector/app/views/main/main.template.html
index c7a90cd..f3bec97 100644
--- a/ui-modules/app-inspector/app/views/main/main.template.html
+++ b/ui-modules/app-inspector/app/views/main/main.template.html
@@ -45,7 +45,7 @@
                                     <i class="fa fa-ban fa-stack-2x text-danger"></i>
                                 </span>
                             </button>
-                            <a href="/brooklyn-ui-blueprint-composer" class="btn btn-sm btn-default entity-tree-action">
+                            <a href="{{ ctrl.composerUrl }}" class="btn btn-sm btn-default entity-tree-action" ng-if="ctrl.composerUrl">
                                 <i class="fa fa-plus"></i>
                             </a>
                         </div>
diff --git a/ui-modules/blueprint-composer/app/components/catalog-selector/catalog-selector-palette-footer.html b/ui-modules/blueprint-composer/app/components/catalog-selector/catalog-selector-palette-footer.html
index fd13c2a..2cb7ddf 100644
--- a/ui-modules/blueprint-composer/app/components/catalog-selector/catalog-selector-palette-footer.html
+++ b/ui-modules/blueprint-composer/app/components/catalog-selector/catalog-selector-palette-footer.html
@@ -23,43 +23,43 @@
     <small class="help-block text-sm palette-footer-message" ng-class="{ 'no-match': searchedItems.length === 0 }">
         <span ng-if="search">
           <span ng-if="searchedItems.length === 0">
-            <span ng-if="!skippingFilters && itemsBeforeActiveFilters.length > itemsAfterActiveFilters.length">
+            <div class="palette-footer-explanation" ng-if="!skippingFilters && itemsBeforeActiveFilters.length > itemsAfterActiveFilters.length"><span>
                     <strong>{{ itemsBeforeActiveFilters.length - itemsAfterActiveFilters.length }} 
                         <ng-pluralize count="itemsBeforeActiveFilters.length" when="{ '1': 'item', 'other': 'items' }"></<ng-pluralize></strong> 
                     matching search but excluded by filters. 
                     <button class="btn btn-outline btn-info" ng-click="disableFilters()">Clear filters</button>
-            </span>
-            <span ng-if="skippingFilters || itemsBeforeActiveFilters.length === 0">
+            </span></div>
+            <div class="palette-footer-explanation" ng-if="skippingFilters || itemsBeforeActiveFilters.length === 0"><span>
                     No matches for <code>{{ search }}</code>.
-            </span>
+            </span></div>
           </span>
           <span ng-if="searchedItems.length > 0">
-            <span ng-if="skippingFilters">
+            <div ng-if="skippingFilters"><span>
                     <strong>No matches with selected filters.</strong><br/>
                     Showing matches ignoring filters.
-            </span>
-            <span ng-if="!skippingFilters && itemsBeforeActiveFilters.length > itemsAfterActiveFilters.length">
+            </span></div>
+            <div class="palette-footer-explanation" ng-if="!skippingFilters && itemsBeforeActiveFilters.length > itemsAfterActiveFilters.length"><span>
                     <strong>{{ itemsBeforeActiveFilters.length - itemsAfterActiveFilters.length }} more 
                         <ng-pluralize count="itemsBeforeActiveFilters.length" when="{ '1': 'item', 'other': 'items' }"></<ng-pluralize></strong> 
                     matching search but excluded by filters.
                     <button class="btn btn-outline btn-info" ng-click="disableFilters()">Clear filters</button>
-            </span>
+            </span></div>
           </span>
         </span>
         <span ng-if="!search">
-            <span ng-if="itemsBeforeActiveFilters.length == 0">
+            <div class="palette-footer-explanation" ng-if="itemsBeforeActiveFilters.length == 0"><span>
                 <strong>Nothing available.</strong>
-            </span>
-            <span ng-if="skippingFilters && searchedItems.length > 0">
+            </span></div>
+            <div class="palette-footer-explanation" ng-if="skippingFilters && searchedItems.length > 0"><span>
                     <strong>Nothing available in selected filters.</strong><br/>
                     Ignoring filters.
-            </span>
-            <span ng-if="!skippingFilters && itemsBeforeActiveFilters.length > itemsAfterActiveFilters.length">
+            </span></div>
+            <div class="palette-footer-explanation" ng-if="!skippingFilters && itemsBeforeActiveFilters.length > itemsAfterActiveFilters.length"><span>
                     <strong>{{ itemsBeforeActiveFilters.length - itemsAfterActiveFilters.length }} more 
                         <ng-pluralize count="itemsBeforeActiveFilters.length" when="{ '1': 'item', 'other': 'items' }"></<ng-pluralize></strong> 
                     excluded by filters. 
                     <button class="btn btn-outline btn-info" ng-click="disableFilters()">Clear filters</button>
-            </span>
+            </span></div>
         </span>
         
     </small>
diff --git a/ui-modules/blueprint-composer/app/components/catalog-selector/catalog-selector.directive.js b/ui-modules/blueprint-composer/app/components/catalog-selector/catalog-selector.directive.js
index 1b2fbdc..799c71b 100644
--- a/ui-modules/blueprint-composer/app/components/catalog-selector/catalog-selector.directive.js
+++ b/ui-modules/blueprint-composer/app/components/catalog-selector/catalog-selector.directive.js
@@ -346,9 +346,10 @@
         if (!rowsPerPage) {
             let palette = angular.element(document.querySelector(".page-main-area"));
             let toolbar = angular.element(document.querySelector(".navbar-mode"));
+            let title = 56;
             let header = angular.element($element[0].querySelector(".catalog-palette-header"));
             let footer = angular.element($element[0].querySelector(".catalog-palette-footer"));
-            rowsPerPage = Math.max(MIN_ROWS_PER_PAGE, Math.floor((palette[0].offsetHeight - (toolbar[0].offsetHeight + header[0].offsetHeight + footer[0].offsetHeight + 16)) / ($scope.state.viewMode.rowHeightPx || 96)));
+            rowsPerPage = Math.max(MIN_ROWS_PER_PAGE, Math.floor((palette[0].offsetHeight - (toolbar[0].offsetHeight + title + header[0].offsetHeight + footer[0].offsetHeight + 32)) / ($scope.state.viewMode.rowHeightPx || 96)));
         }
         $scope.$apply(() => $scope.pagination.itemsPerPage = rowsPerPage * $scope.state.viewMode.itemsPerRow);
     }
diff --git a/ui-modules/blueprint-composer/app/components/catalog-selector/catalog-selector.less b/ui-modules/blueprint-composer/app/components/catalog-selector/catalog-selector.less
index 25f6cda..1a2ac11 100644
--- a/ui-modules/blueprint-composer/app/components/catalog-selector/catalog-selector.less
+++ b/ui-modules/blueprint-composer/app/components/catalog-selector/catalog-selector.less
@@ -233,6 +233,12 @@
         // useful if it wraps, e.g. because pagination buttons show:
         line-height: 2;
         margin-top: -2px;
+        .palette-footer-explanation {
+          line-height: 1.5em;
+          display: flex;
+          flex-direction: column;
+          justify-content: flex-end;
+        }
     }
     div[uib-pagination] {
       margin-left: 1em;
@@ -344,4 +350,5 @@
   p:last-child {
     margin-bottom: 0;
   }
+
 }
diff --git a/ui-modules/blueprint-composer/app/components/catalog-selector/catalog-selector.template.html b/ui-modules/blueprint-composer/app/components/catalog-selector/catalog-selector.template.html
index 3cf0cc9..a78c9fd 100644
--- a/ui-modules/blueprint-composer/app/components/catalog-selector/catalog-selector.template.html
+++ b/ui-modules/blueprint-composer/app/components/catalog-selector/catalog-selector.template.html
@@ -162,7 +162,9 @@
             <p><i class="mini-icon fa fa-fw fa-bookmark"></i> <samp class="type-symbolic-name">{{popover.symbolicName}}</samp></p>
             <p ng-if="popover.version"><i class="mini-icon fa fa-fw fa-code-fork"></i> {{popover.version}}</p>
         </div>
-        <p class="quick-info-description" ng-if="popover.description">{{popover.description}}</p>
+        <p class="quick-info-description">
+            <md-first-line raw-data="::popover.description" raw-item="::popover"></md-first-line>
+        </p>
         <p class="quick-info-description" ng-if="popover == freeFormTile">This is an ad hoc tile for an item entered by the user not known in the catalog.</p>
         <div class="quick-info-metadata bundle">
             <p ng-if="lastUsedText(popover)"><i class="mini-icon fa fa-clock-o"></i> {{ lastUsedText(popover) }}
diff --git a/ui-modules/blueprint-composer/app/components/providers/blueprint-service.provider.js b/ui-modules/blueprint-composer/app/components/providers/blueprint-service.provider.js
index a798d9c..19794ab 100644
--- a/ui-modules/blueprint-composer/app/components/providers/blueprint-service.provider.js
+++ b/ui-modules/blueprint-composer/app/components/providers/blueprint-service.provider.js
@@ -584,6 +584,9 @@
             version: data.containingBundle.split(':')[1]
         });
         entity.miscData.set('typeName', data.displayName || data.symbolicName);
+        entity.miscData.set('displayName', data.displayName);
+        entity.miscData.set('symbolicName', data.symbolicName);
+        entity.miscData.set('description', data.description);
         entity.miscData.set('config', data.config || []);
         entity.miscData.set('parameters', data.parameters || []);
         entity.miscData.set('sensors', data.sensors || []);
diff --git a/ui-modules/blueprint-composer/app/components/spec-editor/spec-editor.directive.js b/ui-modules/blueprint-composer/app/components/spec-editor/spec-editor.directive.js
index 9106aa8..9da1581 100644
--- a/ui-modules/blueprint-composer/app/components/spec-editor/spec-editor.directive.js
+++ b/ui-modules/blueprint-composer/app/components/spec-editor/spec-editor.directive.js
@@ -35,7 +35,7 @@
 const REPLACED_DSL_ENTITYSPEC = '___brooklyn:entitySpec';
 
 angular.module(MODULE_NAME, [onEnter, autoGrow, blurOnEnter, brooklynDslEditor, brooklynDslViewer])
-    .directive('specEditor', ['$rootScope', '$templateCache', '$injector', '$sanitize', '$filter', '$log', '$sce', '$timeout', '$document', '$state', '$compile', 'blueprintService', 'composerOverrides', specEditorDirective])
+    .directive('specEditor', ['$rootScope', '$templateCache', '$injector', '$sanitize', '$filter', '$log', '$sce', '$timeout', '$document', '$state', '$compile', 'blueprintService', 'composerOverrides', 'mdHelper', specEditorDirective])
     .filter('specEditorConfig', specEditorConfigFilter)
     .filter('specEditorType', specEditorTypeFilter)
     .run(['$templateCache', templateCache]);
@@ -89,7 +89,7 @@
     'string', 'boolean', 'integer', 'double', 'duration', ' port'
     ];
 
-export function specEditorDirective($rootScope, $templateCache, $injector, $sanitize, $filter, $log, $sce, $timeout, $document, $state, $compile, blueprintService, composerOverrides) {
+export function specEditorDirective($rootScope, $templateCache, $injector, $sanitize, $filter, $log, $sce, $timeout, $document, $state, $compile, blueprintService, composerOverrides, mdHelper) {
     return {
         restrict: 'E',
         scope: {
@@ -118,6 +118,7 @@
         scope.REPLACED_DSL_ENTITYSPEC = REPLACED_DSL_ENTITYSPEC;
         scope.parameters = [];
         scope.config = {};
+        specEditor.descriptionVisible = false;
         specEditor.paramTypes = PARAM_TYPES;
 
         specEditor.getParameter = getParameter;
@@ -214,6 +215,11 @@
         // Model
         scope.$watch('model', (newVal, oldVal) => {
             if (newVal && !newVal.equals(oldVal)) {
+                scope.modelDescription = mdHelper.analyzeDescription({
+                    description: newVal.miscData.get('description'),
+                    symbolicName: newVal.miscData.get('symbolicName'),
+                    displayName: newVal.miscData.get('displayName'),
+                });
                 loadLocalConfigFromModel();
                 loadLocalParametersFromModel();
             }
@@ -470,16 +476,6 @@
             return issues.some(issue => issue.level === ISSUE_LEVEL.ERROR) ? 'badge-danger' : 'badge-warning';
         };
 
-        specEditor.descriptionHtml = (text) => {
-            let out = [];
-            for (let item of text.split(/\n\n+/)) {
-                out.push('<div class="paragraph-spacing"></div>');
-                out.push($sanitize(item));
-            }
-            out.splice(0, 1);
-            return $sce.trustAsHtml(out.join("\n"));
-        };
-
         function getConfigWidgetModeInternal(item, val) {
             if (angular.element($document[0].activeElement).hasClass("form-control") && item.widgetMode) {
                 // don't switch mode in mid-edit, e.g. if you are manually typing $brooklyn:component("x").config("y")
diff --git a/ui-modules/blueprint-composer/app/components/spec-editor/spec-editor.less b/ui-modules/blueprint-composer/app/components/spec-editor/spec-editor.less
index 3a6e045..68fd6e3 100644
--- a/ui-modules/blueprint-composer/app/components/spec-editor/spec-editor.less
+++ b/ui-modules/blueprint-composer/app/components/spec-editor/spec-editor.less
@@ -32,7 +32,7 @@
     cursor: pointer;
     transition: 0.1s ease all;
     &:hover {
-      color: @brand-primary;
+      color: @brand-primary !important;
     }
   }
   .remove-affordance() {
@@ -903,6 +903,15 @@
         order: 99;
    }
   }
+
+  .type-description {
+    .toolbar-button-affordance();
+    padding-top: 1ex;
+    float: right;
+    &.active {
+      color: @gray;
+    }
+  }
 }
 
 .popover.spec-editor-popover {
diff --git a/ui-modules/blueprint-composer/app/components/spec-editor/spec-editor.template.html b/ui-modules/blueprint-composer/app/components/spec-editor/spec-editor.template.html
index 6c46db0..19b0371 100644
--- a/ui-modules/blueprint-composer/app/components/spec-editor/spec-editor.template.html
+++ b/ui-modules/blueprint-composer/app/components/spec-editor/spec-editor.template.html
@@ -41,16 +41,32 @@
             <img ng-src="{{model.icon}}" alt="{{model | entityName}} logo" class="media-object" />
         </div>
         <div class="media-body panel-header-body">
-            <div title="{{ model.miscData.get('typeName') }}"><i class="fa fa-bookmark panel-header-icon"></i><samp class="entity-type-header">{{model.type}}</samp> <span class="label label-primary version">{{model | entityVersion}}</span></div>
+            <div title="{{ model.miscData.get('typeName') }}">
+                <i class="fa fa-question-circle type-description"
+                   ng-class="{ 'active': specEditor.descriptionVisible }"
+                   title="{{ specEditor.descriptionVisible ? 'Hide description' : 'Show description' }}"
+                   ng-if="modelDescription.isPresent"
+                   ng-click="specEditor.descriptionVisible = !specEditor.descriptionVisible"></i>
+                <i class="fa fa-bookmark panel-header-icon"></i><samp class="entity-type-header">{{model.type}}</samp>
+                <span class="label label-primary version">{{model | entityVersion}}</span>
+            </div>
             <div ng-if="['POLICY', 'ENRICHER'].indexOf(model.family.id) === -1" class="identifier">
                 <i class="fa fa-id-card-o panel-header-icon"></i>
                 <input class="form-control editable" ng-model="model.id" placeholder="(no reference ID)" blur-on-enter />
                 </div>
+            <md-if-oneline ng-show="specEditor.descriptionVisible && modelDescription.isNonMultiline" data="modelDescription"></md-if-oneline>
         </div>
     </div>
   </div>
 </section>
 
+<br-collapsible state="true" ng-show="specEditor.descriptionVisible && modelDescription.isMultiline">
+    <heading>Description</heading>
+
+    <div style="margin-left: 3px;">
+        <md-if-multiline ng-show="specEditor.descriptionVisible" data="modelDescription"></md-if-multiline>
+    </div>
+</br-collapsible>
 
 <!-- ENTITY PARAMETERS -->
 <br-collapsible state="state.parameters.open" ng-if="!model.parent">  <!-- the ng-if is needed to make state update?! -->
@@ -672,7 +688,9 @@
             <p><i class="mini-icon fa fa-fw fa-cog"></i> <samp class="type-symbolic-name">{{item.name}}</samp>
                 <span class="config-type label-color column-for-type oneline label label-success">{{item.type}}</span></p>
         </div>
-        <p class="quick-info-description" ng-if="item.description" ng-bind-html="specEditor.descriptionHtml(item.description)"></p>
+        <p class="quick-info-description">
+            <md-field raw-item="item"/>
+        </p>
         <div class="quick-info-metadata config-default" ng-if="item.defaultValue"></i>Default value: <samp>{{item.defaultValue}}</samp></div>
     </div>
 
@@ -686,7 +704,9 @@
             <p><i class="mini-icon fa fa-fw fa-cog"></i> <samp class="type-symbolic-name">{{item.name}}</samp>
             <span class="config-type label-color column-for-type oneline label label-success">{{item.type}}</span></p>
         </div>
-        <p class="quick-info-description" ng-if="item.description" ng-bind-html="specEditor.descriptionHtml(item.description)"></p>
+        <p class="quick-info-description" ng-if="item.description">
+            <md-field raw-item="item"/>
+        </p>
         <div class="quick-info-metadata config-default" ng-if="item.defaultValue"></i>Default value: <samp>{{item.defaultValue}}</samp></div>
         <div class="quick-info-metadata config-required" ng-if="item.constraints.required"><i>This field is required.</div>
     </div>
diff --git a/ui-modules/blueprint-composer/app/index.js b/ui-modules/blueprint-composer/app/index.js
index e45c07f..7504627 100755
--- a/ui-modules/blueprint-composer/app/index.js
+++ b/ui-modules/blueprint-composer/app/index.js
@@ -35,6 +35,7 @@
 import brooklynUserManagement from 'brooklyn-ui-utils/user-management/user-management';
 import brYamlEditor from 'brooklyn-ui-utils/yaml-editor/yaml-editor';
 import brUtils from 'brooklyn-ui-utils/utils/general';
+import mdHelper from 'brooklyn-ui-utils/md-helper';
 
 import brSpecEditor from './components/spec-editor/spec-editor.directive';
 import brooklynCatalogSaver from './components/catalog-saver/catalog-saver.directive';
@@ -79,7 +80,7 @@
 
 angular.module('brooklynBlueprintComposer', [ngAnimate, ngResource, ngCookies, ngClipboard, uiRouter, 'ui.router.state.events', brCore,
     brServerStatus, brAutoFocus, brIconGenerator, brInterstitialSpinner, brooklynModuleLinks, brooklynUserManagement,
-    brYamlEditor, brUtils, brSpecEditor, brooklynCatalogSaver, brooklynApi, bottomSheet, stackViewer, brDragndrop,
+    brYamlEditor, brUtils, brSpecEditor, brooklynCatalogSaver, brooklynApi, bottomSheet, stackViewer, brDragndrop, mdHelper,
     customActionDirective, customConfigSuggestionDropdown, paletteApiProvider, paletteServiceProvider, blueprintLoaderApiProvider,
     breadcrumbs, catalogSelector, designer, objectCache, entityFilters, locationFilter, actionService, tabService, composerOverrides, blueprintService,
     dslService, paletteDragAndDropService, recentlyUsedService, scriptTagDecorator, brandAngularJs])
diff --git a/ui-modules/blueprint-composer/app/views/main/graphical/graphical.state.less b/ui-modules/blueprint-composer/app/views/main/graphical/graphical.state.less
index 3e5c5b8..dfb6a30 100644
--- a/ui-modules/blueprint-composer/app/views/main/graphical/graphical.state.less
+++ b/ui-modules/blueprint-composer/app/views/main/graphical/graphical.state.less
@@ -147,6 +147,8 @@
     &, .palette-full-height-wrapper {
       display: flex;
       flex-direction: column;
+      justify-content: space-between;
+
       flex: 1 1 auto;
       margin-left: 0;
       margin-right: 0;
diff --git a/ui-modules/blueprint-importer/app/views/main/main.controller.js b/ui-modules/blueprint-importer/app/views/main/main.controller.js
index 959ca2c..ee269b6 100644
--- a/ui-modules/blueprint-importer/app/views/main/main.controller.js
+++ b/ui-modules/blueprint-importer/app/views/main/main.controller.js
@@ -123,7 +123,11 @@
                         type: item.symbolicName + ':' + item.version
                     }]
                 };
-                return '/brooklyn-ui-blueprint-composer/#!/graphical?format=brooklyn-camp&yaml=' + JSON.stringify(yaml);
+                if (brBrandInfo.blueprintComposerBaseUrl) {
+                    return brBrandInfo.blueprintComposerBaseUrl + '#!/graphical?format=brooklyn-camp&yaml=' + JSON.stringify(yaml);
+                } else {
+                    return;
+                }
             default:
                 return;
         }
diff --git a/ui-modules/branding/brand.js b/ui-modules/branding/brand.js
index b38a221..c296f72 100644
--- a/ui-modules/branding/brand.js
+++ b/ui-modules/branding/brand.js
@@ -55,6 +55,7 @@
         getAppDeployedUrl: function (appId, entityId) {
             return '/brooklyn-ui-app-inspector/#!/application/' + appId + '/entity/' + entityId + '/summary';
         },
+        blueprintComposerBaseUrl: '/brooklyn-ui-blueprint-composer/',
     };
 }
 
diff --git a/ui-modules/catalog/app/components/type-item/index.html b/ui-modules/catalog/app/components/type-item/index.html
index 93b57fb..2cffc7b 100644
--- a/ui-modules/catalog/app/components/type-item/index.html
+++ b/ui-modules/catalog/app/components/type-item/index.html
@@ -19,7 +19,7 @@
 <div class="media">
     <a ui-sref="bundle.type({bundleId: bundle.symbolicName, bundleVersion: bundle.version, typeId: type.symbolicName, typeVersion: type.version})"></a>
     <div class="media-left media-middle icon">
-        <img ng-src="{{type | iconGeneratorPipe:'symbolicName'}}" class="media-object" alt="{{::type.symbolicName}}'s logo" />
+        <img ng-src="{{ ::type | iconGeneratorPipe:'symbolicName' }}" class="media-object" alt="{{::type.symbolicName}}'s logo" />
     </div>
     <div class="media-body catalog-item-body">
         <div class="btn-group pull-right" ng-if="showBundle || showType">
@@ -36,8 +36,8 @@
             <span class="label label-default">{{::type.version}}</span>
         </h4>
         <span class="media-description">
-            <span ng-if="type.displayName"><i class="fa fa-fw fa-bookmark sym-name"></i> <samp class="sym-name">{{::type.symbolicName}}</samp>: </span>
-            {{::type.description}}
+            <span ng-if="type.displayName"><i class="fa fa-fw fa-bookmark sym-name"></i> <samp class="sym-name">{{::type.symbolicName}}</samp>{{::descriptionMarkdown.oneline ? ':' : ''}} </span>
+            <md-first-line data="descriptionMarkdown"></md-first-line>
         </span>
     </div>
 </div>
diff --git a/ui-modules/catalog/app/components/type-item/index.js b/ui-modules/catalog/app/components/type-item/index.js
index c916458..145ada4 100644
--- a/ui-modules/catalog/app/components/type-item/index.js
+++ b/ui-modules/catalog/app/components/type-item/index.js
@@ -18,17 +18,18 @@
  */
 import angular from 'angular';
 import template from './index.html';
+import mdHelper from 'brooklyn-ui-utils/md-helper';
 
 import brIconGenerator from 'brooklyn-ui-utils/icon-generator/icon-generator';
 
 const MODULE_NAME = 'brooklyn.components.type-item';
 
-angular.module(MODULE_NAME, [brIconGenerator])
-    .directive('typeItem', typeListDirective);
+angular.module(MODULE_NAME, [brIconGenerator, mdHelper])
+    .directive('typeItem', ['mdHelper', typeListDirective]);
 
 export default MODULE_NAME;
 
-export function typeListDirective() {
+export function typeListDirective(mdHelper) {
     return {
         restrict: 'EA',
         scope: {
@@ -37,6 +38,9 @@
             showType: '<?',
             showBundle: '<?'
         },
-        template: template
+        template: template,
+        controller: ["$scope", ($scope) => {
+           $scope.descriptionMarkdown = mdHelper.analyzeDescription($scope.type);
+        }],
     };
 }
diff --git a/ui-modules/catalog/app/index.js b/ui-modules/catalog/app/index.js
index b294236..9a78a70 100644
--- a/ui-modules/catalog/app/index.js
+++ b/ui-modules/catalog/app/index.js
@@ -28,6 +28,7 @@
 import brooklynModuleLinks from 'brooklyn-ui-utils/module-links/module-links';
 import brooklynUserManagement from "brooklyn-ui-utils/user-management/user-management";
 import brooklynCatalogUpdater from 'brooklyn-ui-utils/catalog-uploader/catalog-uploader';
+import mdHelper from 'brooklyn-ui-utils/md-helper';
 
 import uiRouter from 'angular-ui-router';
 
@@ -39,7 +40,7 @@
 
 const IS_PRODUCTION = process.env.NODE_ENV === 'production' || false;
 
-angular.module('brooklynCatalog', [ngAnimate, ngCookies, ngResource, brCore, brServerStatus, brInterstitialSpinner, brooklynModuleLinks, brooklynUserManagement, brooklynCatalogUpdater, uiRouter, catalogState, catalogBundleState, catalogBundleTypeState, brandAngularJs])
+angular.module('brooklynCatalog', [ngAnimate, ngCookies, ngResource, brCore, brServerStatus, brInterstitialSpinner, brooklynModuleLinks, brooklynUserManagement, brooklynCatalogUpdater, uiRouter, catalogState, catalogBundleState, catalogBundleTypeState, brandAngularJs, mdHelper])
     .config(['$logProvider', '$compileProvider', applicationConfig])
     .config(['$urlRouterProvider', routerConfig])
     .run(['$rootScope', '$state', 'brSnackbar', errorHandler])
diff --git a/ui-modules/catalog/app/views/bundle/bundle.state.js b/ui-modules/catalog/app/views/bundle/bundle.state.js
index 22ccaf0..fb7832e 100644
--- a/ui-modules/catalog/app/views/bundle/bundle.state.js
+++ b/ui-modules/catalog/app/views/bundle/bundle.state.js
@@ -67,6 +67,10 @@
         search: {}
     };
 
+    $scope.pagination = {
+        maxItemsToShow: 50
+    };
+
     $scope.clearSearchFilters = () => {
         $scope.state.search = {};
         $scope.state.orderBy = orderBys[0];
@@ -84,8 +88,9 @@
         });
     };
 
-    $scope.downloadBundle = () => {
-        return catalogApi.downloadBundle($scope.bundle.symbolicName, $scope.bundle.version, {urlOnly: true});
+    $scope.downloadBundleUrl = () => {
+        return !$scope.bundle ? /* loading */ "" :
+            /* normal */ catalogApi.downloadBundle($scope.bundle.symbolicName, $scope.bundle.version, {urlOnly: true});
     }
 
     $scope.isNonEmpty = (o) => {
diff --git a/ui-modules/catalog/app/views/bundle/bundle.template.html b/ui-modules/catalog/app/views/bundle/bundle.template.html
index 47c7683..53ce2b9 100644
--- a/ui-modules/catalog/app/views/bundle/bundle.template.html
+++ b/ui-modules/catalog/app/views/bundle/bundle.template.html
@@ -21,8 +21,8 @@
         <header class="row">
             <div class="col-md-12">
                 <div class="media">
-                    <div class="media-left">
-                        <img ng-src="{{bundle | iconGeneratorPipe:'symbolicName'}}" class="media-object" alt="{{::bundle.symbolicName}}'s logo" />
+                    <div class="media-left" ng-if="bundle.symbolicName">
+                        <img ng-src="{{::bundle | iconGeneratorPipe:'symbolicName'}}" class="media-object" alt="{{::bundle.symbolicName}}'s logo" />
                     </div>
                     <div class="media-body">
                         <div class="pull-right highlights" ng-bind-html="bundle | bundleHighlights"></div>
@@ -37,7 +37,7 @@
                                     </li>
                                 </ul>
                             </div>
-                            <a class="btn btn-sm btn-default" ng-href="{{downloadBundle()}}">
+                            <a class="btn btn-sm btn-default" ng-href="{{downloadBundleUrl()}}">
                                 <i class="fa fa-fw fa-download"></i> Download
                             </a>
                         </h4>
@@ -104,7 +104,8 @@
 
             <div class="col-md-12">
                 <ul class="list-group list-group-types">
-                    <li ng-repeat="type in bundle.types | filter:state.search | orderBy:state.orderBy.id as filteredTypes track by (type.containingBundle + type.symbolicName + type.version)"
+                    <li ng-repeat="type in (bundle.types | filter:state.search | orderBy:state.orderBy.id) as filteredTypes track by (type.containingBundle + type.symbolicName + type.version)"
+                        ng-if="$index < pagination.maxItemsToShow"
                         ng-class="{'deprecated': type.deprecated, 'disabled': type.disabled}"
                         class="list-group-item">
                         <type-item bundle="bundle" type="type" show-type="true"></type-item>
@@ -114,11 +115,21 @@
                         <h4>Loading types</h4>
                     </li>
                     <li ng-show="bundle.types.length === 0" class="list-group-item empty">
-                        <h4>There is no types associated to this bundle</h4>
+                        <h4>There are no types provided by this bundle</h4>
                     </li>
                     <li ng-show="bundle.types.length > 0 && filteredTypes.length === 0" class="list-group-item no-results">
                         <h4>No results matching current filters</h4>
                     </li>
+
+                    <!-- quick and dirty pagination; should improve / refactor / apply to others; but for now we are only dealing with bundles
+                         that might have 1000 entities so only focused on that -->
+                    <div ng-if="filteredTypes.length > pagination.maxItemsToShow" style="text-align: right;">
+                        <i>Only showing {{ pagination.maxItemsToShow }} of {{ filteredTypes.length }}</i>
+                        &nbsp;&nbsp;
+                        <a ng-click="pagination.maxItemsToShow = pagination.maxItemsToShow + 50">Show more</a>
+                        &nbsp;&nbsp;
+                        <a ng-click="pagination.maxItemsToShow = filteredTypes.length">Show all</a>
+                    </div>
                 </ul>
             </div>
         </div>
diff --git a/ui-modules/catalog/app/views/bundle/type/type.state.js b/ui-modules/catalog/app/views/bundle/type/type.state.js
index 478e439..99052e6 100644
--- a/ui-modules/catalog/app/views/bundle/type/type.state.js
+++ b/ui-modules/catalog/app/views/bundle/type/type.state.js
@@ -17,6 +17,7 @@
  * under the License.
  */
 import angular from 'angular';
+import ngSanitize from "angular-sanitize";
 import template from './type.template.html';
 import modalTemplate from './modal.template.html';
 import brooklynTypeItem from '../../../components/type-item/index';
@@ -26,11 +27,12 @@
 import brooklynQuickLaunch from 'brooklyn-ui-utils/quick-launch/quick-launch';
 import brTable from 'brooklyn-ui-utils/table/index';
 import brUtils from 'brooklyn-ui-utils/utils/general';
+import mdHelper from 'brooklyn-ui-utils/md-helper';
 import {HIDE_INTERSTITIAL_SPINNER_EVENT} from 'brooklyn-ui-utils/interstitial-spinner/interstitial-spinner';
 
 const MODULE_NAME = 'type.state';
 
-angular.module(MODULE_NAME, [brooklynCatalogApi, brooklynQuickLaunch, brooklynTypeItem, brUtils, brTable])
+angular.module(MODULE_NAME, [ngSanitize, brooklynCatalogApi, brooklynQuickLaunch, brooklynTypeItem, brUtils, brTable, mdHelper])
     .provider('locationApi', locationApiProvider)
     .config(['$stateProvider', typeStateConfig]);
 
@@ -40,7 +42,7 @@
     name: 'bundle.type',
     url: '/types/:typeId/:typeVersion',
     template: template,
-    controller: ['$scope', '$state', '$stateParams', '$q', '$uibModal', 'brBrandInfo', 'brUtilsGeneral', 'brSnackbar', 'catalogApi', typeController],
+    controller: ['$scope', '$state', '$stateParams', '$q', '$uibModal', 'brBrandInfo', 'brUtilsGeneral', 'brSnackbar', 'catalogApi', 'mdHelper', typeController],
     controllerAs: 'ctrl'
 };
 
@@ -48,7 +50,7 @@
     $stateProvider.state(bundleState);
 }
 
-export function typeController($scope, $state, $stateParams, $q, $uibModal, brBrandInfo, brUtilsGeneral, brSnackbar, catalogApi) {
+export function typeController($scope, $state, $stateParams, $q, $uibModal, brBrandInfo, brUtilsGeneral, brSnackbar, catalogApi, mdHelper) {
     $scope.state = {
         default: 2,
         limit: 2
@@ -71,6 +73,8 @@
         return brUtilsGeneral.isNonEmpty(o);
     };
 
+    $scope.composerUrl = brBrandInfo.blueprintComposerBaseUrl;
+
     $scope.deploy = (event)=> {
         let instance = $uibModal.open({
             template: modalTemplate,
@@ -125,6 +129,7 @@
                 typeVersion: typeVersion.version
             };
         });
+        $scope.typeDescription = mdHelper.analyze( ($scope.type || {}).description );
         
         $scope.$emit(HIDE_INTERSTITIAL_SPINNER_EVENT);
     }).catch(error => {
@@ -133,6 +138,7 @@
     });
     
     $scope.tables = {};
+    $scope.markdown = mdHelper.analyze;
     ['config', 'sensors', 'effectors'].forEach((t) => $scope.tables[t] = { columns: [] });
     
     function addColumn(cols, base) {
@@ -167,6 +173,7 @@
             field: 'description',
             width: 150,
             colspan: 6,
+            template: '<md-field raw-data="::item[column.field]"></md-field>',
             tdClass: 'column-for-description',
         } );
         
diff --git a/ui-modules/catalog/app/views/bundle/type/type.template.html b/ui-modules/catalog/app/views/bundle/type/type.template.html
index 028151e..f15a273 100644
--- a/ui-modules/catalog/app/views/bundle/type/type.template.html
+++ b/ui-modules/catalog/app/views/bundle/type/type.template.html
@@ -20,15 +20,15 @@
     <header class="row">
         <div class="col-md-12">
             <div class="media">
-                <div class="media-left">
-                    <img ng-src="{{type | iconGeneratorPipe:'symbolicName'}}" class="media-object" alt="{{::type.symbolicName}}'s logo" />
+                <div class="media-left" ng-if="type.symbolicName">
+                    <img ng-src="{{::type | iconGeneratorPipe:'symbolicName'}}" class="media-object" alt="{{::type.symbolicName}}'s logo" />
                 </div>
                 <div class="media-body">
                     <h4 class="media-heading">
                         <i class="fa fa-star" ng-if="type.template" aria-label="This item is a template"></i>
                         {{::(type.displayName || type.symbolicName)}}
                         <div class="btn-group version-dropdown" uib-dropdown dropdown-append-to-body="true">
-                            <button uib-dropdown-toggle id="versions-dropdown" type="button" class="btn btn-sm btn-default"><span><i class="fa fa-code-fork"></i> {{::type.version}}</span> 
+                            <button uib-dropdown-toggle id="versions-dropdown" type="button" class="btn btn-sm btn-default"><span><i class="fa fa-code-fork"></i> {{::type.version}}</span>
                                 <span class="caret" ng-if="versions && versions.length > 1"></span></button>
                             <ul class="dropdown-menu dropdown-menu-right versions-dropdown-list" uib-dropdown-menu role="menu" aria-labelledby="versions-dropdown">
                                 <li role="menuitem" ng-repeat="version in versions track by (version.bundleSymbolicName + version.bundleVersion + version.typeVersion)" ng-class="{'active': version.bundleSymbolicName + ':' + version.bundleVersion === type.containingBundle && version.typeVersion === type.version}">
@@ -54,7 +54,7 @@
                         <i class="fa fa-fw fa-exclamation-circle"></i> This type has the following aliases:
                         <code ng-repeat="alias in type.aliases track by alias">{{::alias}}</code>
                     </small></p>
-                    <p class="media-description type-description">{{::type.description}}</p>
+                    <md-if-oneline data="typeDescription"></md-if-oneline>
                     <p class="media-types">
                         <span ng-repeat="supertype in type.supertypes | limitTo:state.limit track by supertype" class="label label-supertype pull-left">
                             <i class="fa fa-fw fa-puzzle-piece "></i>
@@ -76,11 +76,11 @@
                 <div class="btn-group" uib-dropdown ng-if="isEditable()">
                     <button uib-dropdown-toggle type="button" class="btn btn-primary"><i class="fa fa-fw fa-edit"></i> Edit <span class="caret"></span></button>
                     <ul class="dropdown-menu dropdown-menu-right" uib-dropdown-menu role="menu" aria-labelledby="edit-dropdown">
-                        <li role="menuitem" ng-if="type.supertypes.includes('org.apache.brooklyn.api.entity.Entity')">
-                            <a ng-href="/brooklyn-ui-blueprint-composer/#!/graphical?bundleSymbolicName={{type.containingBundle.split(':')[0]}}&bundleVersion={{type.containingBundle.split(':')[1]}}&typeSymbolicName={{type.symbolicName}}&typeVersion={{type.version}}">Using graphical designer</a>
+                        <li role="menuitem" ng-if="type.supertypes.includes('org.apache.brooklyn.api.entity.Entity') && composerUrl">
+                            <a ng-href="{{ composerUrl }}#!/graphical?bundleSymbolicName={{type.containingBundle.split(':')[0]}}&bundleVersion={{type.containingBundle.split(':')[1]}}&typeSymbolicName={{type.symbolicName}}&typeVersion={{type.version}}">Using graphical designer</a>
                         </li>
-                        <li role="menuitem" ng-if="type.supertypes.includes('org.apache.brooklyn.api.entity.Entity')">
-                            <a ng-href="/brooklyn-ui-blueprint-composer/#!/yaml?bundleSymbolicName={{type.containingBundle.split(':')[0]}}&bundleVersion={{type.containingBundle.split(':')[1]}}&typeSymbolicName={{type.symbolicName}}&typeVersion={{type.version}}">Using YAML editor</a>
+                        <li role="menuitem" ng-if="type.supertypes.includes('org.apache.brooklyn.api.entity.Entity') && composerUrl">
+                            <a ng-href="{{ composerUrl }}#!/yaml?bundleSymbolicName={{type.containingBundle.split(':')[0]}}&bundleVersion={{type.containingBundle.split(':')[1]}}&typeSymbolicName={{type.symbolicName}}&typeVersion={{type.version}}">Using YAML editor</a>
                         </li>
                         <li role="menuitem" ng-if="type.supertypes.includes('org.apache.brooklyn.api.location.Location')">
                             <a ng-href="brooklyn-ui-location-manager/#!/location?symbolicName={{type.symbolicName}}&version={{type.version}}">Using location manager</a>
@@ -92,6 +92,11 @@
                 </button>
             </div>
             <uib-tabset active="tab">
+                <uib-tab heading="Description" ng-if="typeDescription.isMultiline">
+                    <md-if-multiline data="::typeDescription"></md-if-multiline>
+                    <div style="padding-bottom: 48px;"></div>
+                </uib-tab>
+
                 <uib-tab heading="Config" ng-if="isNonEmpty(type.config)">
                     <br-table ng-model="type.config" columns="tables.config.columns" col-width="20"></br-table>
                 </uib-tab>
diff --git a/ui-modules/catalog/app/views/catalog/catalog.state.js b/ui-modules/catalog/app/views/catalog/catalog.state.js
index f7a6072..9edd821 100644
--- a/ui-modules/catalog/app/views/catalog/catalog.state.js
+++ b/ui-modules/catalog/app/views/catalog/catalog.state.js
@@ -22,6 +22,7 @@
 import brooklynTypeItem from '../../components/type-item/index';
 import brUtils from 'brooklyn-ui-utils/utils/general';
 import template from './catalog.template.html';
+import {analyzeDescription} from 'brooklyn-ui-utils/md-helper';
 import {HIDE_INTERSTITIAL_SPINNER_EVENT} from 'brooklyn-ui-utils/interstitial-spinner/interstitial-spinner';
 
 const MODULE_NAME = 'catalog.state';
@@ -88,8 +89,8 @@
         }
 
     $scope.pagination = {
-        page: 1,
-        itemsPerPage: 20
+        page: 1, // not used
+        itemsPerPage: 20  // used as an absolute limit
     };
     $scope.config = {
         orderBy: savedOrderBy.orderBy === 'bundles' ? orderBysBundles : orderBysTypes
@@ -201,8 +202,10 @@
             return;
         }
         if (input.description) {
-            return input.description;
+            // bundles don't have description yet so this is moot, but when they do this will be nice - or better use the md-if-multiline widget from mdHelper
+            return analyzeDescription(input).oneline;
         }
+
         let alwaysGenerateDefaultDescription = true;
         if (alwaysGenerateDefaultDescription || (input.symbolicName && input.symbolicName.startsWith('brooklyn-catalog-bom'))) {
             // useful in anonymous case because the name gives no clue as to the title;
diff --git a/ui-modules/catalog/app/views/catalog/catalog.template.html b/ui-modules/catalog/app/views/catalog/catalog.template.html
index 5a726fa..cee272b 100644
--- a/ui-modules/catalog/app/views/catalog/catalog.template.html
+++ b/ui-modules/catalog/app/views/catalog/catalog.template.html
@@ -69,7 +69,7 @@
         </div>
 
         <div class="col-md-12">
-            <ul class="list-group list-group-bundles" ng-show="state.view === 'bundles'">
+            <ul class="list-group list-group-bundles" ng-if="state.view === 'bundles'">
                 <li ng-repeat="bundle in bundles | filter:state.search | orderBy:state.orderBy.id as filteredBundles track by (bundle.symbolicName + bundle.version)"
                         ng-class="{'expanded': !isCollapsed, 'is-expandable': bundle.types.length > 0}"
                         class="list-group-item bundles-list">
@@ -98,6 +98,7 @@
                     </div>
 
                     <div class="extra" uib-collapse="isCollapsed">
+                        <!-- note entities are not filtered as search changes; but they will take an initial search; not intuitive -->
                         <div ng-init="filteredEntities = (bundle.types | bundleTypeFilter:'org.apache.brooklyn.api.entity.Entity' | filter:state.search)" ng-show="::filteredEntities.length > 0">
                             <ul class="list-group list-group-types">
                                 <li class="list-group-item typed-group-table-header">
@@ -105,11 +106,21 @@
                                               when="{'one': 'Entity', 'other': 'Entities'}">
                                     </ng-pluralize>
                                 </li>
-                                <li ng-repeat="entity in filteredEntities track by (entity.containingBundle + entity.symbolicName + entity.version)"
+                                <li ng-repeat="entity in ::filteredEntities track by (entity.containingBundle + entity.symbolicName + entity.version)"
+                                        ng-if="!isCollapsed && $index < pagination.itemsPerPage"
                                         ng-class="{'deprecated': entity.deprecated, 'disabled': entity.disabled}"
                                         class="list-group-item">
-                                    <type-item bundle="bundle" type="entity"></type-item>
+                                    <type-item bundle="::bundle" type="::entity"></type-item>
                                 </li>
+                                <!-- quick and dirty pagination; should improve / refactor / apply to others; but for now we are only dealing with bundles
+                                     that might have 1000 entities so only focused on that -->
+                                <div ng-if="::filteredEntities.length > pagination.itemsPerPage" style="text-align: right;">
+                                    <i>Only showing {{ pagination.itemsPerPage }} of {{ ::filteredEntities.length }}</i>
+                                    &nbsp;&nbsp;
+                                    <a ng-click="pagination.itemsPerPage = pagination.itemsPerPage + 20">Show more</a>
+                                    &nbsp;&nbsp;
+                                    <a ng-click="pagination.itemsPerPage = filteredEntities.length">Show all</a>
+                                </div>
                             </ul>
                         </div>
                         <div ng-init="filteredPolicies = (bundle.types | bundleTypeFilter:'org.apache.brooklyn.api.policy.Policy' | filter:state.search)" ng-show="::filteredPolicies.length > 0">
@@ -165,7 +176,7 @@
                 </li>
             </ul>
 
-            <ul class="list-group list-group-types" ng-show="state.view === 'types'">
+            <ul class="list-group list-group-types" ng-if="state.view === 'types'">
                 <li ng-repeat="type in types | filter:state.search | orderBy:state.orderBy.id  as filteredTypes track by (type.containingBundle + type.symbolicName + type.version)"
                     ng-class="{'deprecated': type.deprecated, 'disabled': type.disabled}"
                     class="list-group-item">
@@ -176,7 +187,7 @@
                     <h4>Loading types</h4>
                 </li>
                 <li ng-show="types.length === 0" class="list-group-item empty">
-                    <h4>There is no items in the catalog</h4>
+                    <h4>There are no items in the catalog</h4>
                     <button class="btn btn-primary" ng-click="launchCatalogUploader()">Upload some</button>
                 </li>
                 <li ng-show="filteredTypes.length === 0" class="list-group-item no-results">
diff --git a/ui-modules/utils/icon-generator/icon-generator.js b/ui-modules/utils/icon-generator/icon-generator.js
index 0a20e94..5775da9 100644
--- a/ui-modules/utils/icon-generator/icon-generator.js
+++ b/ui-modules/utils/icon-generator/icon-generator.js
@@ -31,7 +31,10 @@
 export default MODULE_NAME;
 
 export function iconGeneratorProvider() {
-    let useSessionStorage = true;
+    // session storage can fill up very quickly with icons - best not to use it.
+    // generation isn't that big a problem anyway, across tabs.
+    // actual icons are probably more expensive, but the browser will cache those for us.
+    let useSessionStorage = false;
     let background = [0, 0, 0, 0];
     let margin = 0.2;
     let size = 128;
@@ -52,6 +55,10 @@
             useSessionStorage = false;
             return this;
         },
+        enableSessionStorage: function () {
+            useSessionStorage = true;
+            return this;
+        },
         $get: ['$cacheFactory', function ($cacheFactory) {
             if (useSessionStorage && typeof(Storage) !== "undefined") {
                 return new IconGenerator(new SessionsStorageWrapper(CACHE_NAME), background, margin, size);
@@ -191,16 +198,38 @@
     this.get = getValue;
     this.put = putValue;
 
+    var disabled = null;
+    var count = 0;
+
+    // session storage can fill up very quickly with icons - so have some good error checking
+
     function getValue(key, defaultValue) {
+        //if (count++ % 1000 < 10) console.log("SessionStorage access for "+key+": "+( sessionStorage.getItem(cacheName + '.' + key) ? "hit" : "miss" )+" (count "+count+")");
         return sessionStorage.getItem(cacheName + '.' + key) || defaultValue || undefined;
     }
 
     function putValue(key, value) {
+        //if (count++ % 1000 < 10) console.log("SessionStorage write for "+key+": "+( sessionStorage.getItem(cacheName + '.' + key) ? "already present" : "not present" )+" (count "+count+")");
+        if (disabled) {
+            if (disabled + 60*1000 < Date.now()) {
+                console.log("Attempting to re-enable session storage");
+                disabled = null;
+            } else {
+                return;
+            }
+        }
         try {
             sessionStorage.setItem(cacheName + '.' + key, value);
         } catch (ex) {
+            console.warn("Error setting cache value in session storage; will try clearing and retrying", cacheName, key, ex);
             sessionStorage.clear();
-            this.putValue(key, value);
+            try {
+                sessionStorage.setItem(cacheName + '.' + key, value);
+                console.log("Succeeded after clearing cache");
+            } catch (ex2) {
+                console.warn("Failed even after clearing cache; will disable session storage for a period", ex2);
+                disabled = Date.now();
+            }
         }
     }
 }
diff --git a/ui-modules/utils/md-helper/index.js b/ui-modules/utils/md-helper/index.js
new file mode 100644
index 0000000..4890749
--- /dev/null
+++ b/ui-modules/utils/md-helper/index.js
@@ -0,0 +1,190 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+import angular from 'angular';
+import marked from 'marked';
+
+const MODULE_NAME = 'br.utils.md-helper';
+
+angular.module(MODULE_NAME, [])
+    .factory("mdHelper", mdHelperProvider)
+    .directive('mdField', [ mdFieldDirective])
+    .directive('mdFirstLine', [ mdFirstLineDirective])
+    .directive('mdIfOneline', [ mdIfOnelineDirective])
+    .directive('mdIfMultiline', [mdIfMultilineDirective]);
+
+export default MODULE_NAME;
+
+// displays markdown, as one-line or multi-line depending on the data
+export function mdFieldDirective() {
+    return {
+        restrict: 'E',
+        scope: {
+            data: '<',
+            rawData: '<',
+            rawItem: '<',
+        },
+        controller: ['$scope', populateData],
+        template: `
+            <div>
+                <md-if-oneline data="data"></md-if-oneline>
+                <md-if-multiline data="data"></md-if-multiline>
+            </div>
+        `,
+    };
+}
+
+// prints out one line of data -- in future could use markdown formatting, but currently does not
+export function mdFirstLineDirective() {
+    return {
+        restrict: 'E',
+        scope: {
+            data: '<',
+            rawData: '<',
+            rawItem: '<',
+        },
+        controller: ['$scope', populateData],
+        template: `
+            <span>{{::data.oneline}}</span>
+        `,
+    };
+}
+
+// prints out full, formatted, _if_ it is one line of data
+export function mdIfOnelineDirective() {
+    return {
+        restrict: 'E',
+        scope: {
+            data: '<',
+            rawData: '<',
+            rawItem: '<',
+        },
+        controller: ['$scope', populateData],
+        template: `
+            <div ng-if="data.isNonMultiline">
+                <p ng-if="data.unformatted">{{data.unformatted}}</p>
+                <div ng-if="data.markdownFormatted" ng-bind-html="data.markdownFormatted"></div>
+            </div>
+        `,
+    };
+}
+
+// prints out all the data, formatted, if multiline
+export function mdIfMultilineDirective() {
+    return {
+        restrict: 'E',
+        scope: {
+            data: '<',
+            rawData: '<',
+            rawItem: '<',
+        },
+        controller: ['$scope', populateData],
+        // for multiline, collapse margin from children eg an <h1> first element, inserting then removing a 24px margin
+        template: `
+            <div ng-if="data.isMultiline">
+                <pre ng-if="data.unformatted">{{data.unformatted}}</pre>
+                <div ng-if="data.markdownFormatted" style="margin-top: -24px;"><div style="margin-top: 24px;" ng-bind-html="data.markdownFormatted"></div></div>
+            </div>
+        `,
+    };
+}
+
+function nameFieldValues(src) {
+    src = src || {};
+    return ['symbolicName', 'displayName', 'typeName', 'name'].map(x => src[x]);
+}
+
+function populateData($scope) {
+    if (!$scope.data) {
+        if (!$scope.rawData && $scope.rawItem) {
+            $scope.rawData = $scope.rawItem.description;
+        }
+        $scope.data = analyze($scope.rawData, nameFieldValues($scope.rawItem));
+    }
+}
+
+export function analyzeDescription(input) {
+    input = input || {};
+    return analyze(input.description, nameFieldValues(input));
+}
+
+export function analyze(field, names) {
+    let result = {
+        isPresent: !!field,
+    };
+    result.isMultiline = !!(result.isPresent && (field.trim().match(/^#+($| )/) || field.split('\n', 5)>5));
+    result.isNonMultiline = result.isPresent && !result.isMultiline;
+    if (result.isPresent) {
+        try {
+            result.markdownFormatted = marked(field);
+        } catch (e) {
+            console.log("could not convert markdown; treading as unformatted", e);
+            // not markdown
+            result.unformatted = field;
+        }
+    }
+    result.oneline = oneline(field, names);
+    return result;
+}
+
+function containsAny(line, words) {
+    if (!words) return false;
+    return words.filter(w => w && line.indexOf(w)>=0);
+}
+
+export function oneline(field, names) {
+    if (!field) {
+        return null;
+    }
+    if (field.trim().match(/^#+($| )/)) {
+        // looks like markdown; skip line if it's a title
+        let inputStripped = field.trim().substring(1);
+        let line1 = inputStripped.split('\n', 1)[0].trim();
+
+        if (!line1 || (containsAny(line1, names) && line1.length<100)) {
+            // probably a summary eg "About FooEntity" -- ignore
+            inputStripped = /\n((.*)(\n.*)*)/.exec(inputStripped)[1]
+            if (!inputStripped) return null;
+            return oneline(inputStripped, names);
+        }
+
+        // otherwise use default behaviour
+
+        field = line1;
+    }
+
+    let dStrippedP = field.trim().split('\n', 2);
+    let d = dStrippedP[0];
+
+    if (d.length > 200) {
+        // if very long then truncate and return...
+        return d.substring(0,197)+"...";
+    } else {
+        if (dStrippedP[1] !== undefined) {
+            d += " ...";
+        }
+        return d;
+    }
+}
+
+function mdHelperProvider() {
+    return {
+        analyze,
+        analyzeDescription,
+    }
+}
\ No newline at end of file
diff --git a/ui-modules/utils/package.json b/ui-modules/utils/package.json
index a895f62..408731f 100644
--- a/ui-modules/utils/package.json
+++ b/ui-modules/utils/package.json
@@ -36,6 +36,7 @@
     "angular": "^1.6.1",
     "angular-animate": "^1.6.1",
     "angular-multiple-transclusion": "^1.0.0",
+    "angular-sanitize": "^1.6.1",
     "angular-ui-bootstrap": "^2.5.0",
     "bootstrap": "^3.3.7",
     "codemirror": "^5.27.2",
@@ -44,6 +45,7 @@
     "jsonschema": "^1.1.1",
     "jssha": "^2.2.0",
     "lodash": "^4.15.0",
+    "marked": "^2.0.1",
     "rxjs": "^5.0.0-beta.11"
   },
   "devDependencies": {
diff --git a/ui-modules/utils/quick-launch/quick-launch.js b/ui-modules/utils/quick-launch/quick-launch.js
index 89ea39f..907cffe 100644
--- a/ui-modules/utils/quick-launch/quick-launch.js
+++ b/ui-modules/utils/quick-launch/quick-launch.js
@@ -41,10 +41,10 @@
             args: '=?', // default behaviour of code is: { noEditButton: false, noComposerButton: false, noCreateLocationLink: false, location: null }
             callback: '=?',
         },
-        controller: ['$scope', '$http', '$location', 'brSnackbar', controller]
+        controller: ['$scope', '$http', '$location', 'brSnackbar', 'brBrandInfo', controller]
     };
 
-    function controller($scope, $http, $location, brSnackbar) {
+    function controller($scope, $http, $location, brSnackbar, brBrandInfo) {
         $scope.deploying = false;
         $scope.model = {
             newConfigFormOpen: false,
@@ -287,13 +287,17 @@
         }
 
         function openComposer() {
+            if (!brBrandInfo.blueprintComposerBaseUrl) {
+              console.warn("Composer unavailable in this build");
+              return;
+            }
             try {
-              window.location.href = '/brooklyn-ui-blueprint-composer/#!/graphical?'+
+              window.location.href = brBrandInfo.blueprintComposerBaseUrl + '#!/graphical?'+
                 ($scope.app.plan.format ? 'format='+encodeURIComponent($scope.app.plan.format)+'&' : '')+
                 'yaml='+encodeURIComponent(buildComposerYaml(true));
             } catch (error) {
               console.warn("Opening composer in YAML text editor mode because we cannot generate a model for this configuration:", error);
-              window.location.href = '/brooklyn-ui-blueprint-composer/#!/yaml?'+
+              window.location.href = brBrandInfo.blueprintComposerBaseUrl + '#!/yaml?'+
                 ($scope.app.plan.format ? 'format='+encodeURIComponent($scope.app.plan.format)+'&' : '')+
                 'yaml='+encodeURIComponent(
                     "# This plan may have items which require attention so is being opened in YAML text editor mode.\n"+