Skip to content

@sqlrooms/documents

Artifact-scoped Markdown documents, structured block documents, and knowledge-index utilities for SQLRooms.

Usage

tsx
import {
  BlockDocumentArtifact,
  BlockDocumentsSliceConfig,
  BlockDocumentChartRendererProvider,
  BlockDocumentStatefulBlockRendererProvider,
  DocumentsSliceConfig,
  buildKnowledgeIndex,
  createBlockDocumentCommands,
  createBlockDocumentFeatureSlices,
  createDocumentCommands,
  createDocumentsSlice,
  createMarkdownDocumentBlockDefinition,
} from '@sqlrooms/documents';
import {createDocumentsCrdtMirror} from '@sqlrooms/documents/crdt';
import {
  createArtifactTypeFromStatefulBlock,
  defineArtifactTypes,
} from '@sqlrooms/artifacts';

const documentBlockDefinition = createMarkdownDocumentBlockDefinition();

const artifactTypes = defineArtifactTypes({
  document: createArtifactTypeFromStatefulBlock(documentBlockDefinition),
  'block-document': {
    label: 'Block Document',
    defaultTitle: 'Block Document',
    component: BlockDocumentArtifact,
    onCreate: ({artifactId, store}) => {
      store.getState().blockDocuments.ensureBlockDocument(artifactId);
    },
    onEnsure: ({artifactId, store}) => {
      store.getState().blockDocuments.ensureBlockDocument(artifactId);
    },
    onDelete: ({artifactId, store}) => {
      store.getState().blockDocuments.removeBlockDocument(artifactId);
    },
  },
});

const roomStore = createRoomStore(
  persistSliceConfigs(
    {
      name: 'my-room',
      sliceConfigSchemas: {
        documents: DocumentsSliceConfig,
        blockDocuments: BlockDocumentsSliceConfig,
      },
    },
    (set, get, store) => ({
      ...createDocumentsSlice()(set, get, store),
      ...createBlockDocumentFeatureSlices({
        onDeleteOwnedStatefulBlock: ({
          blockType,
          blockInstanceId,
          getState,
        }) => {
          if (blockType === 'dashboard') {
            getState().mosaicDashboard.removeDashboard(blockInstanceId);
          }
        },
      })(set, get, store),
    }),
  ),
);

MarkdownDocument uses the Tiptap-backed MarkdownDocumentEditor. It keeps Markdown as the controlled value, renders a rich document editing surface, and keeps the existing CodeMirror source panel for direct Markdown edits.

MarkdownDocumentEditor is also exported as a reusable controlled editor:

tsx
<MarkdownDocumentEditor
  value={markdown}
  assets={assets}
  onChange={setMarkdown}
/>

The rich editor is the primary surface. The optional Markdown source panel can be opened alongside it and edits the same canonical Markdown string:

tsx
<MarkdownDocumentEditor
  value={markdown}
  onChange={setMarkdown}
  sourcePanelOpen={showSource}
  onSourcePanelOpenChange={setShowSource}
/>

Document Markdown can reference document-owned assets with asset:// URLs:

md
![Revenue by week](asset://chart-revenue-week)

Pass the document asset map to MarkdownDocumentEditor to render those links as browser-loadable image data while preserving the canonical asset:// link in Markdown source. MarkdownDocument handles this automatically for artifacts stored in the documents slice.

The documents slice exposes upsertAsset, removeAsset, and getAsset for managing image assets alongside Markdown content. SVG assets may use utf8 or base64 encoding; PNG assets must use base64 encoding.

Block Documents

createBlockDocumentsSlice() exposes structured state for artifact types backed by composable blocks: text, lists, images, standalone Mosaic/vgplot charts, and direct stateful blocks such as dashboards, pivots, or Markdown documents.

The shared block vocabulary lives in @sqlrooms/blocks. @sqlrooms/documents builds on those contracts with the concrete Tiptap-backed BlockDocument editor, persistence slice, commands, and AI authoring helpers.

Block documents persist Tiptap/ProseMirror JSON as their canonical content and provide block DTO helpers for command and AI authoring surfaces:

tsx
import {
  BlockDocumentsSliceConfig,
  createAddBlockDocumentTextBlockTool,
  createBlockDocumentFeatureSlices,
  createListBlockDocumentBlocksTool,
  createMoveBlockDocumentBlockTool,
} from '@sqlrooms/documents';

const roomStore = createRoomStore(
  persistSliceConfigs(
    {
      name: 'my-room',
      sliceConfigSchemas: {
        blockDocuments: BlockDocumentsSliceConfig,
      },
    },
    (set, get, store) => ({
      ...createBlockDocumentFeatureSlices()(set, get, store),
    }),
  ),
);

Generic AI helpers use the same block DTOs as commands and the editor. Hosts provide a small BlockDocumentAiAdapter that ensures a document, lists its blocks, and appends new blocks; feature packages or apps can then compose these tools with their own stateful-block tools. Reorder tools require the narrower BlockDocumentMoveBlockAiAdapter capability:

ts
const tools = {
  add_block_document_text_block: createAddBlockDocumentTextBlockTool({
    blockDocumentAdapter,
    blockDocumentId,
  }),
  list_block_document_blocks: createListBlockDocumentBlocksTool({
    blockDocumentAdapter,
    blockDocumentId,
  }),
  move_block_document_block: createMoveBlockDocumentBlockTool({
    blockDocumentAdapter,
    blockDocumentId,
  }),
};

BlockDocumentAiAdapter.addBlock may return a block ID synchronously or from a promise. Hosts that already expose block-document mutations as room commands can therefore use createBlockDocumentCommandAiAdapter to invoke the canonical block-document.append-blocks and block-document.move-block commands while keeping generic AI tools package-neutral:

ts
const blockDocumentAdapter = createBlockDocumentCommandAiAdapter({
  store,
});

Hosts with persisted compatibility artifact types can pass isBlockDocumentArtifact without adding that product vocabulary to the shared adapter API.

Block-Scoped Ask AI

startBlockScopedChat(...) opens or reuses an artifact-scoped AI session for a specific block in a block document. It is exported from @sqlrooms/documents so hosts can wire Ask AI buttons, block header actions, or context menus without duplicating session-selection rules.

The helper intentionally depends on a small action adapter instead of importing a room store. Hosts provide artifact validation, session ownership, prompt updates, and assistant visibility through StartBlockScopedChatActions:

ts
import {
  startBlockScopedChat,
  type StartBlockScopedChatActions,
} from '@sqlrooms/documents';

const actions: StartBlockScopedChatActions = {
  getArtifact: (artifactId) =>
    store.getState().artifacts.getArtifact(artifactId),
  getCurrentArtifactId: () => store.getState().artifacts.currentArtifactId,
  setCurrentArtifact: (artifactId) =>
    store.getState().artifacts.setCurrentArtifact(artifactId),
  getAiSessions: () => store.getState().ai.config.sessions,
  getAiSessionArtifacts: () => store.getState().artifactAi.sessionArtifacts,
  createArtifactScopedSession: () =>
    store.getState().artifactAi.createArtifactScopedSession(),
  switchSession: (sessionId) => store.getState().ai.switchSession(sessionId),
  getSessionDraftContextItemIds: (sessionId) =>
    store.getState().ai.getSessionDraftContextItemIds(sessionId),
  setSessionDraftContextItemIds: (sessionId, ids) =>
    store.getState().ai.setSessionDraftContextItemIds(sessionId, ids),
  setPrompt: (sessionId, prompt) =>
    store.getState().ai.setPrompt(sessionId, prompt),
  startAnalysisWhenReady: (sessionId) =>
    store.getState().ai.startAnalysisWhenReady(sessionId),
};

await startBlockScopedChat({
  target: {
    blockDocumentId,
    blockId,
    blockType: 'map',
    blockInstanceId: mapId,
  },
  prompt: 'Repair this map',
  revealAssistant: () => setAssistantOpen(true),
  actions,
  isValidBlockDocumentArtifact: (artifact) =>
    artifact.type === 'block-document',
});

isValidBlockDocumentArtifact is required because hosts may use product-specific artifact type names or compatibility aliases. The helper validates the target artifact before mutating UI state, switches to the target artifact when needed, and derives the block context item with blockContextItemId(...) unless contextItemId is provided.

Only running sessions for the same artifact and context item block a new Ask AI turn. Finished sessions that already contain the block context are reused, their prompt is replaced, and their draft context is left untouched if it already includes the block item. When a matching running session exists, the helper switches to it, shows a toast, and returns without revealing the assistant, changing the prompt, or starting another analysis.

The slice can create block documents, replace the Tiptap JSON body, and append/insert/update/remove/reorder top-level blocks. Supported block DTOs include headings, paragraphs, lists, todos, images, chart images, standalone chart blocks, and direct stateful blocks.

BlockDocumentArtifact and BlockDocumentEditor provide the first rich editor surface for this structured state. BlockDocumentArtifact injects an editable, non-movable title node into the Tiptap document and reports title changes through onTitleChange, so hosts can keep artifact metadata and tab labels in sync. The editor owns Tiptap nodes for SQLRooms custom blocks, but chart and stateful block rendering are host-provided so @sqlrooms/documents does not import Mosaic, pivot, or other feature packages:

tsx
<BlockDocumentChartRendererProvider renderer={MosaicBlockDocumentChartRenderer}>
  <BlockDocumentStatefulBlockRendererProvider
    renderers={{
      dashboard: DashboardBlockRenderer,
      pivot: PivotBlockRenderer,
    }}
    blockTypes={[
      {
        blockType: 'dashboard',
        label: 'Dashboard',
        description: 'Interactive dashboard',
        createNode: (blockId) => ({
          type: 'blockDocumentStatefulBlock',
          attrs: {
            id: blockId,
            blockType: 'dashboard',
            blockInstanceId: createDashboardBlockState(blockId),
            ownership: 'owned',
            caption: '',
          },
        }),
      },
    ]}
  >
    <BlockDocumentArtifact
      artifactId={blockDocumentArtifactId}
      title="Analysis"
      onTitleChange={(title) =>
        renameBlockDocument(blockDocumentArtifactId, title)
      }
    />
  </BlockDocumentStatefulBlockRendererProvider>
</BlockDocumentChartRendererProvider>

Stateful blocks carry document-local label/binding attributes, surfaced to renderers via BlockDocumentStatefulBlockRendererProps:

  • caption — the block's user-facing label in the document flow (onCaptionChange).
  • tableName — the table a table-bound block reads from (e.g. data-table), resolved via db.findTable like the chart block's tableName (onTableNameChange). Block types that keep their data binding inside their own backing state leave this unset.

If no renderer is registered, chart and stateful blocks render a clear unsupported state while preserving their Tiptap JSON attributes. blockTypes controls the host-specific entries shown in the plus menu. Chart renderers also receive a selected flag so their controls can reflect whether the block is the active Tiptap node selection. When a block is converted through the handle menu, custom createNode callbacks receive an optional {initialText} value with the source block text; hosts can use it to seed stateful blocks such as embedded Markdown documents. Stateful block types can opt into persisted vertical resizing with resizableHeight, defaultHeight, minHeight, and maxHeight; the editor stores the resulting height on the block node and renders a bottom resize handle just below the block for writable documents. Interactive blocks can also opt into requireScrollModifier; ordinary wheel gestures then keep scrolling the document and show a short hint, while Cmd+scroll on macOS or Ctrl+scroll elsewhere scrolls nested overflow regions inside the block. Use scrollHintLabel to customize the hint target text.

The backing instance owns its own display name. Hosts should seed that name when creating the backing state (for example from the registered block type's label or command defaultTitle) and resolve it from the instance when a UI needs the current name. Shared block-document state does not persist a stateful-block title mirror.

Block and panel definitions can provide reusable settings components. The host settings shell is owned by @sqlrooms/documents, while feature packages own the actual settings UI. Settings components receive the selected blockId, optional parent dashboardId, optional blockInstanceId, and an optional onClose callback when the host shell can be collapsed. Custom controls that should reveal the settings shell can call blockSettings.requestOpenSettingsPanel(). Controls that represent the currently shown settings can read blockSettings.runtime.isSettingsPanelOpen and call blockSettings.requestCloseSettingsPanel() to toggle the shell closed. Hosts that want the standard resizable side panel shell can wrap their surface with BlockSettingsPanelLayout; pass editor and documentId when rendering outside a BlockDocumentEditor context, or omit them for dashboard panels that use SelectablePanelWrapper.

createBlockDocumentFeatureSlices() composes createBlockDocumentsSlice() with the shared createBlockSettingsSlice() for apps that want a block document surface with reusable settings. If an app also uses another feature helper that includes block settings, install the shared settings slice only once by using one feature helper plus the other feature's lower-level slice.

Stateful Blocks

Use a statefulBlock block when the document should host a stateful SQLRooms surface directly, without wrapping it in an artifact shell:

ts
blockDocuments.appendBlocks(blockDocumentArtifactId, [
  {
    id: 'pivot-block',
    type: 'statefulBlock',
    blockType: 'pivot',
    blockInstanceId: 'pivot-instance-1',
    ownership: 'owned',
    caption: 'Pivot table',
  },
]);

Hosts provide renderers through BlockDocumentStatefulBlockRendererProvider:

tsx
<BlockDocumentStatefulBlockRendererProvider
  renderers={{
    pivot: PivotBlockRenderer,
    dashboard: DashboardBlockRenderer,
  }}
  blockTypes={[
    {
      blockType: 'pivot',
      label: 'Pivot Table',
      description: 'Embedded pivot table',
    },
  ]}
>
  <BlockDocumentArtifact
    artifactId={blockDocumentArtifactId}
    title="Embedded Report"
    onTitleChange={(title) =>
      renameBlockDocument(blockDocumentArtifactId, title)
    }
  />
</BlockDocumentStatefulBlockRendererProvider>

Top-level artifacts should wrap stateful blocks or block containers at the workspace/tab layer. Block documents host the stateful block directly instead of embedding an artifact shell.

Owned stateful blocks are lifecycle-managed by the host app. Pass onCreateOwnedStatefulBlock to initialize feature state when a new owned block reference appears, and onDeleteOwnedStatefulBlock to clean it up when an owned block is removed from a document or when its owning block document is deleted. Blocks with ownership: 'shared' or ownership: 'external' are not cleaned up by the documents slice. Captions stay local to the block document. Backing instance names are changed through the owning feature's UI or commands, not by editing a block attribute. Stateful block renderers receive onCaptionChange when a writable document lets the embedded surface edit the document-local caption.

The editor normalizes pasted or duplicated owned stateful blocks by assigning fresh top-level block IDs and fresh blockInstanceId values when a duplicate owned instance would otherwise point at the same backing state.

Standalone Chart Blocks

Standalone chart blocks are meant for focused, in-document charts. They store the target tableName, a Mosaic ChartConfig, an optional caption, and an optional selectionGroupId:

ts
blockDocuments.appendBlocks(blockDocumentArtifactId, [
  {
    id: 'revenue-histogram',
    type: 'chart',
    tableName: 'sales',
    config: {
      chartType: 'histogram',
      settings: {field: 'revenue'},
    },
    selectionGroupId: 'overview',
    caption: 'Revenue distribution',
  },
]);

Hosts can render these blocks with the same Mosaic/vgplot chart implementation and settings UI used inside dashboard panels, without embedding a full dashboard. Charts with the same selectionGroupId in one block document share a crossfilter selection. Charts without a group get independent document/block-scoped selections.

Hosted Dashboards

Use a statefulBlock block when the document needs a multi-panel interactive dashboard. The block instance id should map to dashboard state in the host app's Mosaic slice, while the top-level artifact shell remains optional for workspace navigation.

Standalone chart blocks are best for one chart with local context. Dashboard stateful blocks are best for coordinated multi-panel views, richer dashboard layout, or when dashboard AI tools are the natural authoring path.

Commands

createDocumentCommands() registers AI- and palette-friendly commands for document artifacts:

  • document.list
  • document.get
  • document.create
  • document.set-markdown
  • document.append-markdown

createBlockDocumentCommands() registers commands for structured blocks document artifacts. By default the command IDs are:

  • block-document.list
  • block-document.get
  • block-document.create
  • block-document.append-blocks
  • block-document.insert-blocks
  • block-document.update-block
  • block-document.remove-block
  • block-document.move-block
  • block-document.create-chart-block
  • block-document.create-stateful-block

Hosts can pass artifactType, artifactLabel, and commandNamespace options to expose the same command surface under product-specific names while keeping the package API generic.

Hosts can pass statefulBlockTypes to expose supported feature-backed block types to block-document.create-stateful-block.

Block mutation command results include the full refreshed document data plus focused mutation payloads such as blockId, blockIds, blockType, blockTypes, and affectedBlocks. Chart and stateful block creation also return follow-up IDs such as tableName, blockInstanceId, statefulBlockType, the seed instanceTitle, and chosen caption values.

Structured block payloads may include an optional intent string. Use it for the durable natural-language purpose of an agent- or command-created block, such as the question a chart should answer or the job an embedded dashboard should serve. It is persisted with the block, unlike transient mutation metadata.

CRDT

@sqlrooms/documents/crdt exposes Loro Mirror bindings for document state:

ts
createCrdtSlice({
  mirrors: {
    documentState: createDocumentsCrdtMirror(),
  },
});

createDocumentsCrdtMirror() syncs Markdown document bodies, block document Tiptap JSON content, document-owned assets, standalone chart block configs, block document/document artifact metadata, and document artifact tab order. The current artifact selection is kept local.

By default, the mirror treats block-document artifacts as block documents. Hosts with their own artifact type names can pass blockDocumentArtifactTypes, for example:

ts
createDocumentsCrdtMirror({
  blockDocumentArtifactTypes: ['report'],
});

Hosted dashboard state should continue to use the host app's Mosaic persistence, or a future Mosaic-specific CRDT mirror.

Knowledge Index

buildKnowledgeIndex is a pure derived index. It does not persist data.

ts
const index = buildKnowledgeIndex({
  documents: roomStore.getState().documents.config,
  artifacts: roomStore.getState().artifacts.config,
});

It extracts [[Document Title]] wikilinks, body hashtags such as #metrics, and optional frontmatter tags. Links are resolved against document artifact titles. Missing or ambiguous titles are reported as unresolved links.

Classes

Type Aliases

Variables

Functions

References

DocumentAssetType

Renames and re-exports DocumentAsset


DocumentsSliceConfigType

Renames and re-exports DocumentsSliceConfig


MarkdownDocumentStateType

Renames and re-exports MarkdownDocumentState


BlockDocumentBlockType

Renames and re-exports BlockDocumentBlock


BlockDocumentType

Renames and re-exports BlockDocument


BlockDocumentContentType

Renames and re-exports BlockDocumentContent


BlockDocumentMarkType

Renames and re-exports BlockDocumentMark


BlockDocumentNodeType

Renames and re-exports BlockDocumentNode


BlockDocumentsSliceConfigType

Renames and re-exports BlockDocumentsSliceConfig


BlockDocumentChartHeaderActionsRenderer

Renames and re-exports BlockDocumentBlockHeaderActionsRenderer


BlockDocumentStatefulBlockHeaderActionsRenderer

Renames and re-exports BlockDocumentBlockHeaderActionsRenderer