HISTORY_MARKDOWN_ARCHITECTURE

Galaxy Notebooks & Reports: Architecture & Feature Summary

Branch: history_pages Date: 2026-04-01 Status: Feature-complete, pre-merge


1. What Are Galaxy Notebooks?

Galaxy Notebooks are markdown documents tied to Galaxy histories. They let users (and AI agents) document, annotate, and share analysis narratives alongside the data that produced them. A history can have multiple notebooks, and each notebook supports an AI assistant that can read history contents and propose edits.

UI terminology vs backend model: The backend uses the existing Page model for everything — a “Galaxy Notebook” is a Page with history_id set, and a “Report” is a standalone Page. User-facing strings are centralized in Page/constants.ts via PAGE_LABELS, making the terminology easy to change without touching backend code.

This unified model means notebooks and reports share the same editor, revision system, AI chat, and API surface. The difference is contextual: notebooks gain access to history-aware AI tools and are accessed through the history panel rather than the reports grid.

Every save creates an immutable revision, edits from humans and agents are tracked separately via edit_source, and the revision system supports preview and one-click rollback.


2. Reports vs Galaxy Notebooks

The system supports two contexts through a single unified editor (PageEditorView):

AspectReports (standalone)Galaxy Notebooks (history-attached)
Entry pointGrid list (/pages/list) or direct URLHistory panel “Galaxy Notebooks” button
Route/pages/editor?id=X/histories/:historyId/pages/:pageId
history_idnullSet — scopes notebook to a history
AI chat toolsText editing only (no history tools)Full history tools: list_history_datasets, get_dataset_info, get_dataset_peek, get_collection_structure, resolve_hid
Drag-and-dropFrom toolbox directivesAlso from history panel (datasets/collections)
Permissions modalYes (ObjectPermissionsModal)No — inherits from history sharing
Save & ViewYes (slug-based published URL)No (history context, no slug)
ListGrid (/pages/list)Inline HistoryPageList within history panel
Window Manager”View in Window” grid actionClick from list opens in WinBox
Auto-createNoresolveCurrentPage() creates on first visit

Both modes share: editor UI, revision system, AI chat, dirty tracking, diff views, and the same API endpoints.


3. User Stories

Researcher documenting an analysis

“I ran a ChIP-seq pipeline and want to write up what I did and what the results mean, with embedded dataset previews and plots, right next to the history that contains the data.”

AI-assisted notebook editing

“I have 50 datasets from a variant-calling run. I want the AI to summarize what’s in my history and draft a methods section.”

Sharing and publishing

“My analysis is complete. I want to share the notebook read-only.”

Reviewing past AI conversations

“I had a great chat with the AI last week about my RNA-seq results. I want to pick up where I left off.”

Revision history and rollback

“The agent’s last edit broke my formatting. I want to go back to the previous version.”


4. Architecture Overview

+---------------------------------------------------------------------------+
|                          Frontend (Vue 3)                                  |
|                                                                           |
|  HistoryCounter --> HistoryPageView --> PageEditorView <-- PageEditor      |
|       |                 |    |              |                  |           |
|       |           +-----+    +-----+   MarkdownEditor     (report          |
|       |           |                |    TextEditor          entry)         |
|       |     HistoryPageList        |    (drag-drop)                       |
|       |                      SplitView                                    |
|       |                            |                                      |
|       |   PageRevisionList    PageChatPanel                               |
|       |   PageRevisionView     |         |         |                      |
|       |                  ChatMessageCell  ProposalDiffView                 |
|       |                  ChatInput       SectionPatchView                  |
|       |                  PageChatHistoryList                               |
|       |                                                                   |
|  pageEditorStore (Pinia) <---> API Client (api/pages.ts)                  |
+---------------------------------+-----------------------------------------+
                                  | REST
+---------------------------------v-----------------------------------------+
|                       Backend (FastAPI)                                    |
|                                                                           |
|  /api/pages (history_id filter) --> PageManager                           |
|  /api/pages/{id}/revisions      --> PageManager (revisions)               |
|  /api/chat (page_id)            --> ChatManager + AgentService            |
|                                          |                                |
|                                  PageAssistantAgent                       |
|                                    +- list_history_datasets               |
|                                    +- get_dataset_info                    |
|                                    +- get_dataset_peek                    |
|                                    +- get_collection_structure            |
|                                    +- resolve_hid                         |
|                                                                           |
|  Models: Page (+ history_id), PageRevision (+ edit_source), ChatExchange  |
|  markdown_util.py: ready_galaxy_markdown_for_export()                     |
+---------------------------------------------------------------------------+

5. Data Model

Page (extended)

ColumnTypeNotes
idint PK
user_idint FK -> galaxy_userIndexed
history_idint FK -> historyNullable, indexed. When set, page is a Galaxy Notebook
titletextNot versioned
slugtextIndexed. Reports only
latest_revision_idint FK -> page_revisionEager-loaded; circular FK with use_alter
source_invocation_idint FK -> workflow_invocationNullable. Tracks “generated from invocation”
published / importableboolReport sharing features
deletedboolSoft-delete pattern
create_time / update_timedatetime

Relationships: user, history (optional), revisions (cascade delete), latest_revision (eager), source_invocation, tags, annotations, ratings, users_shared_with

PageRevision (extended)

ColumnTypeNotes
idint PK
page_idint FK -> pageIndexed
titletextSnapshot of title at revision time
contenttextRaw markdown with internal IDs
content_formatvarchar(32)"markdown" or "html"
edit_sourcevarchar(16)New. "user", "agent", or "restore"
create_time / update_timedatetime

ChatExchange (extended)

ColumnTypeNotes
page_idint FK -> pageNullable, indexed. Scopes chat to a page

The page_id FK scopes chat exchanges to a page.

Migration

MigrationPurpose
b75f0f4dbcd4Add history_id to page, edit_source to page_revision, page_id to chat_exchange

6. Content Pipeline

Page content has three representations across the boundary, not two. The DB-internal form is distinct from both API fields — a distinction the earlier draft of this doc collapsed (see correction note below).

INBOUND (create/update):
  client markdown (ENCODED ids) --ready_galaxy_markdown_for_import--> decode_id --> DB
  e.g. history_dataset_id=f2db41e1fa331b3e  ----------------------->  history_dataset_id=42

DB-internal (page_revision.content):
  RAW integer ids, directives un-expanded     history_dataset_id=42   (never returned to clients)

OUTBOUND (show/update response) -- rewrite_content_for_export():
        DB-internal ---ready_galaxy_markdown_for_export--> two forms, BOTH with ENCODED ids:
           |
           +--> content_editor = export_markdown
           |      ENCODED ids, directives intact, NOT embed-expanded   (editor reads/writes this)
           |
           +--> content = export_markdown_embed_expanded
                  ENCODED ids + embedded dataset content               (Markdown renderer uses this)

The API returns both fields in PageDetails:

Correction (2026-06-12): An earlier revision of this section described content_editor as “raw, same as DB content” with history_dataset_id=42. That is wrong. The raw integer form exists only in the page_revision.content DB column; the service always runs rewrite_content_for_export before returning, so both API fields carry encoded ids. The real difference between content_editor and content is embed expansion (un-expanded directives vs. inlined dataset content), not raw-vs-encoded ids. Verified against managers/pages.py:457 (rewrite_content_for_export), services/pages.py:89 (_page_to_details), and managers/markdown_util.py:111,655 (import decode_id / export encode_id). This matters: any consumer reading content_editor (the editor, and any MCP page tool) stays in encoded-id space — no raw DB ids leak across the API boundary.


7. Agent Architecture

PageAssistantAgent

Registered as AgentType.PAGE_ASSISTANT in the Galaxy agent framework. Uses pydantic-ai with structured output.

Tools (5):

ToolPurposeReturns
list_history_datasetsPaginated history item listingHID, name, type, state, size, internal ID
get_dataset_infoDetailed metadata for one HIDName, format, state, size, tool info, metadata
get_dataset_peekPre-computed content previewFirst lines of dataset content
get_collection_structureCollection element listingElement names, types, states
resolve_hidHID -> directive argument conversionhistory_dataset_id=N or history_dataset_collection_id=N + job_id

When editing a report (no history_id), history tools are unavailable — the agent can still do full-replacement and section-patch edits on the content.

Output types (3, discriminated by mode literal):

TypeWhen UsedContent
FullReplacementEditComplete document rewriteFull new markdown document
SectionPatchEditTargeted heading-level editTarget heading + new section content
str (plain text)Conversational responseNo edit proposal

System prompt is dynamically assembled:

  1. Static instructions from prompts/page_assistant.md
  2. Auto-generated directive reference table (reads markdown_parse.VALID_ARGUMENTS at runtime)
  3. Current page content injected as context
  4. History name and item count summary (when history_id is set)

The agent works in HID-space (matching what users see in the history panel) and uses resolve_hid to translate to the history_dataset_id=N directive arguments that Galaxy’s markdown renderer expects.

Chat Persistence

Conversations are scoped per-page via ChatExchange.page_id. The flow:

  1. User sends message -> POST /api/chat with page_id and agent_type="page_assistant"
  2. API looks up page, extracts history_id and current content from the page record
  3. Agent processes with history tools (if notebook) and current document context
  4. Response stored as ChatExchange + ChatExchangeMessage with full agent_response JSON
  5. Frontend persists exchange_id in userLocalStorage per-page for session continuity

Chat History Browsing

Users can browse, resume, and delete past conversations for a page via PageChatHistoryList. The store tracks pageChatHistory, isLoadingChatHistory, showChatHistory, and chatHistoryError state. Chat history is fetched from GET /api/chat/page/{page_id}/history and displayed in a sidebar list with timestamps. Selecting a past exchange resumes it; deletion is also supported.


8. Frontend Components

Component Tree

Galaxy Notebook entry:
  HistoryCounter (button in history panel)
    +- HistoryPageView (list + display routing -- 177 lines)
         +- HistoryPageList (notebook picker -- 91 lines)
         +- Markdown (display-only render)
         +- PageEditorView (edit mode delegation)

Report entry:
  PageEditor (thin wrapper -- 12 lines)
    +- PageEditorView

PageEditorView (unified editor -- 372 lines)
  +- ClickToEdit (inline title editing)
  +- MarkdownEditor
  |    +- TextEditor (drag-and-drop for history items)
  +- PageRevisionList (sidebar panel -- 87 lines)
  +- PageRevisionView (revision preview + diff -- 194 lines)
  +- SplitView (resizable 60/40 split -- 92 lines, shared in Common/)
  |    +- PageChatPanel (agent chat -- 570 lines)
  |         +- ChatMessageCell (shared from ChatGXY)
  |         +- ChatInput (shared from ChatGXY)
  |         +- ProposalDiffView (full-doc diff -- 122 lines)
  |         +- SectionPatchView (per-section diff -- 206 lines)
  |         +- PageChatHistoryList (chat history browser -- 234 lines)
  +- ObjectPermissionsModal (reports only -- 17 lines)
  |    +- ObjectPermissions (363 lines)
  |    +- PermissionObjectType (31 lines)
  |    +- SharingIndicator (72 lines)

PageEditorView (unified editor)

The core editor component. Adapts based on context:

FeatureGalaxy Notebook (historyId set)Report (standalone)
Back button target/histories/:hid/pages/pages/list
Title displayHistory name (read-only header)Inline ClickToEdit
Revisions buttonAlwaysAlways
Chat buttonWhen agents configuredWhen agents configured
Permissions buttonHiddenShown
Save & ViewHiddenShown
PreviewNavigates with displayOnly=trueOpens in Window Manager or navigates

View states (template branching):

  1. Loading spinner (no current page yet)
  2. Error alert (dismissible)
  3. Display-only mode (read-only Markdown render + toolbar with Edit button)
  4. Revision view (revision preview/diff with Restore button — supports three view modes)
  5. Edit mode (toolbar + editor + optional chat/revision sidepanels)

Revision view modes (revisionViewMode):

HistoryPageView (notebook context router)

Routes between three states for Galaxy Notebooks:

  1. List mode (no pageId) -> HistoryPageList
  2. Display mode (displayOnly=true) -> Markdown renderer with toolbar
  3. Edit mode (pageId set, no displayOnly) -> delegates to PageEditorView

Handles Window Manager integration: when WM is active, clicking a notebook in the list opens it in a WinBox window via displayOnly=true with router.push(url, { title, preventWindowManager: false }).

Pinia Store (pageEditorStore)

Mode: mode: "history" | "standalone""history" = Galaxy Notebook, "standalone" = Report.

State management:

Cross-session persistence (userLocalStorage):

Smart defaults:

Mode differentiation is minimal — mostly determines UI labels (via PAGE_LABELS) and available features:

Diff System (sectionDiffUtils.ts — 218 lines)

Built on jsdiff (diff@^8.0.3).

FunctionPurpose
markdownSections(content)Split document by #{1,6} headings
computeLineDiff(old, new)Line-level unified diff
sectionDiff(old, new)Per-section change detection
applySectionPatches(old, new, accepted)Merge only accepted section changes
applySectionEdit(content, heading, newContent)Replace single section
diffStats(changes)Count additions/deletions

Stale proposal detection: Uses DJB2 hash of original content. If page content changes after a proposal was generated, Accept buttons are disabled.

Routes

PathComponentNotes
/histories/:historyId/pagesHistoryPageViewNotebook list
/histories/:historyId/pages/:pageIdHistoryPageViewNotebook edit
/histories/:historyId/pages/:pageId?displayOnly=trueHistoryPageViewNotebook read-only (WM)
/pages/editor?id=XPageEditorReport edit
/pages/editor?id=X&displayOnly=truePageEditorReport display
/pages/listGridPageReports grid
/published/page?id=XPageViewPublished report view

Drag-and-Drop

TextEditor supports drag from the history panel when mode="page":

Window Manager Integration

When Galaxy’s Window Manager (WinBox) is active:


9. API Surface

All operations use the unified /api/pages endpoints. Notebooks are pages with history_id set; reports have history_id null.

Page CRUD

MethodPathPurpose
GET/api/pagesList pages (supports history_id filter)
POST/api/pagesCreate page (with optional history_id)
GET/api/pages/{id}Get page (two content fields: content + content_editor)
PUT/api/pages/{id}Update (creates new revision with edit_source)
DELETE/api/pages/{id}Soft-delete
PUT/api/pages/{id}/undeleteRestore

Revisions

MethodPathPurpose
GET/api/pages/{id}/revisionsList revisions
GET/api/pages/{id}/revisions/{rid}Get revision content
POST/api/pages/{id}/revisions/{rid}/revertRestore to revision (edit_source="restore")

Sharing & Publishing (reports only)

MethodPathPurpose
GET/api/pages/{id}/sharingCurrent sharing status
PUT/api/pages/{id}/enable_link_accessEnable link sharing
PUT/api/pages/{id}/publishPublish page
PUT/api/pages/{id}/share_with_usersShare with specific users
PUT/api/pages/{id}/slugSet URL slug

Chat

MethodPathPurpose
POST/api/chatSend message (with page_id + agent_type)
GET/api/chat/page/{page_id}/historyRetrieve page chat history

Index Query Parameters

ParamDefaultNotes
history_idnullFilter by history (the key filter for notebooks)
show_owntrueShow user’s own pages
show_publishedtrueShow published pages
show_sharedfalseShow pages shared with user
searchnullFreetext search
sort_bycreate_time, title, update_time, username
limit / offset100 / 0Pagination

10. Test Coverage

Summary

LayerTestsLOCCoverage
Selenium E2E30669Navigation, editing, drag-drop, WM, revisions, rename, chat, permissions
API integration30336CRUD, revisions, permissions, chat persistence
Agent unit39810Structured output, tools, prompt injection, history context, live LLM
History tools32511All 5 tool functions
Chat manager8149Page-scoped persistence, filtering
Vitest (components)11 test files~3,673All PageEditor components, store, diff utils

Frontend Test Files

FileLinesFocus
pageEditorStore.test.ts1,422Store: CRUD, revisions, persistence, report + notebook modes, chat history
PageEditorView.test.ts649Unified editor: report + notebook modes, revisions, WM
HistoryPageView.test.ts367Notebook list/display/edit routing, lifecycle, WM integration
PageChatPanel.test.ts385Chat loading, proposals, feedback, staleness
sectionDiffUtils.test.ts265Section parsing, diff computation, patch application
PageRevisionList.test.ts207Revision list rendering, source labels, restore
HistoryPageList.test.ts185Notebook list, create/select/view events
SectionPatchView.test.ts68Section-level patch UI
ProposalDiffView.test.ts66Full-replacement diff rendering
PageRevisionView.test.ts199Revision preview + diff modes
PageChatHistoryList.test.ts229Chat history list rendering, selection, deletion
SplitView.test.ts41Resizable split layout

Test Infrastructure


11. ChatGXY Extraction

The existing ChatGXY.vue (982 lines) was refactored into shared sub-components before building the page chat panel:

ComponentLinesPurpose
ChatMessageCell.vue345Message rendering with role styling, feedback buttons, action suggestions
ChatInput.vue96Textarea + send button with busy state
ActionCard.vue97Action suggestion cards with priority-based styling
agentTypes.ts60Agent type registry with icons and labels
chatTypes.ts26Shared ChatMessage interface
chatUtils.ts13generateId() and scrollToBottom() helpers

PageChatPanel reuses all extracted components with no duplication.


12. Design Decisions

Unified Page Model

Both Galaxy Notebooks and Reports reuse the existing Page model rather than introducing a separate table. The distinction is a nullable page.history_id FK — when set, the page is a notebook; when null, it’s a report.

Benefit: One model, one API, one editor, one store. No duplication.

HID Syntax (Decided: Not stored)

Pages store history_dataset_id=X (matching existing Page syntax) rather than hid=N. The agent uses resolve_hid as a tool to bridge between user-visible HIDs and directive IDs. This avoids complex resolution machinery while preserving the agent’s ability to work with HIDs naturally.

Trade-off: power users hand-editing markdown see opaque IDs, but the toolbox and drag-and-drop handle insertion — most users never read raw markdown.

UI Convergence

The legacy PageEditorMarkdown.vue (Options API, local state, no revisions/chat) was replaced by a single unified editor:

Multiple Notebooks Per History

No unique constraint on page.history_id. A history can have multiple notebooks for different analysis perspectives, collaborators, or document types.

Title Not Versioned

Title lives on Page, not on revisions. Renaming doesn’t create a new revision — it’s page identity, not content. (PageRevision does have a title field for snapshot purposes.)

Revision = Append-Only

Every edit (user save, agent apply, restore) creates a new PageRevision. No in-place updates. edit_source tracks provenance.

Section-Level Patching

The agent can propose section-level edits (targeted by heading). The frontend shows per-section diffs with individual checkboxes. Users accept/reject sections independently. This is more practical than all-or-nothing for large documents.

Panel Mutual Exclusion

The revision panel and chat panel are mutually exclusive — toggling one closes the other. This avoids layout complexity and keeps the editor area usable.


13. File Inventory

Backend (Python)

FileLinesRole
lib/galaxy/model/__init__.py+100Page model extensions (history_id, revision edit_source, ChatExchange page_id)
lib/galaxy/managers/pages.py~795PageManager: CRUD, revisions, content pipeline, history filtering
lib/galaxy/webapps/galaxy/api/pages.py~393REST endpoints including revision operations
lib/galaxy/webapps/galaxy/services/pages.py214PagesService: endpoint logic
lib/galaxy/schema/schema.py+120Pydantic schemas: PageDetails, PageRevisionSummary/Details, query payloads
lib/galaxy/managers/markdown_util.py1,434ready_galaxy_markdown_for_export()
lib/galaxy/agents/page_assistant.py453PageAssistantAgent class + structured output types
lib/galaxy/agents/history_tools.py2875 async history data tools
lib/galaxy/agents/prompts/page_assistant.md158System prompt template
lib/galaxy/managers/chat.py394Page-scoped chat methods
lib/galaxy/webapps/galaxy/api/chat.py583Chat endpoints (including page chat history)
lib/galaxy/agents/base.py+1PAGE_ASSISTANT enum value

Frontend (TypeScript/Vue)

FileLinesRole
client/src/components/Page/constants.ts137Centralized UI labels: “Galaxy Notebook” vs “Report” terminology
client/src/api/pages.ts166Unified API client (CRUD, revisions, revert)
client/src/stores/pageEditorStore.ts563Pinia store with mode, persistence, revisions, chat history
client/src/components/PageEditor/PageEditorView.vue372Unified editor (notebook + report)
client/src/components/PageEditor/HistoryPageView.vue177Notebook context: list + display routing
client/src/components/PageEditor/HistoryPageList.vue91Notebook picker for history
client/src/components/PageEditor/PageChatPanel.vue570Agent chat panel + chat history integration
client/src/components/PageEditor/PageChatHistoryList.vue234Chat history browser with selection/deletion
client/src/components/Common/SplitView.vue92Resizable 60/40 split layout
client/src/components/PageEditor/PageRevisionList.vue87Revision sidebar
client/src/components/PageEditor/PageRevisionView.vue194Revision preview + diff view modes
client/src/components/PageEditor/ProposalDiffView.vue122Full-document diff
client/src/components/PageEditor/SectionPatchView.vue206Section-level diff with checkboxes
client/src/components/PageEditor/sectionDiffUtils.ts217Diff computation
client/src/components/PageEditor/ObjectPermissions.vue363Permission checking (reports)
client/src/components/PageEditor/ObjectPermissionsModal.vue17Modal wrapper for permissions
client/src/components/PageEditor/PermissionObjectType.vue31Object type display in permissions
client/src/components/PageEditor/SharingIndicator.vue72Permission toggle indicator
client/src/components/PageEditor/object-permission-composables.ts89Composables for permission handling
client/src/components/PageEditor/PageEditor.vue12Report entry wrapper
client/src/components/Markdown/Editor/TextEditor.vue+40Drag-and-drop additions
ChatGXY extractions (6 files)~637Shared chat components

Tests

FileLinesTests
lib/galaxy_test/selenium/test_history_pages.py66930
lib/galaxy_test/api/test_pages_history_attached.py33630
lib/galaxy/selenium/navigates_galaxy.py+10013 helper methods
test/unit/app/test_agents.py81039 (17 PageAssistantAgent)
test/unit/app/test_history_tools.py51132
test/unit/app/test_chat_manager.py1498
Client vitest (11 files)~3,673Store, components, diff utils
client/src/stores/pageEditorStore.test.ts1,422~87

14. Remaining Work

Not Yet Implemented

ItemScopeNotes
Window Manager chatFrontendPop chat into WinBox, postMessage sync
CodeMirror 6 upgradeFrontendReplace textarea with CM6 for inline diff/suggestions
Streaming responsesFull-stackSSE/WebSocket for agent output; requires CM6
Orchestrator integrationBackendPage agent as sub-agent in workflow orchestrator

Known Issues