Skip to content

Galaxy Notebooks: Persistent Narrative for Human-AI Collaborative Science in Galaxy#22361

Open
jmchilton wants to merge 11 commits intogalaxyproject:devfrom
jmchilton:history_pages
Open

Galaxy Notebooks: Persistent Narrative for Human-AI Collaborative Science in Galaxy#22361
jmchilton wants to merge 11 commits intogalaxyproject:devfrom
jmchilton:history_pages

Conversation

@jmchilton
Copy link
Copy Markdown
Member

Executive Summary

The Problem

Galaxy histories capture computation but not understanding. A history shows datasets and tool runs -- the what happened. It doesn't capture why one approach was chosen over another, which results matter, or the iterative reasoning that led to insights.

This gap is becoming critical. AI agents can already drive Galaxy analyses end-to-end -- running tools, inspecting outputs, chaining steps. But the evolving understanding that emerges from human-AI collaboration has nowhere to live. Chat transcripts evaporate. The agent's rationale disappears. We risk a world where agents democratize analysis but worsen the reproducibility crisis -- generating results without provenance, methods sections, or narrative context.

What We Built

History Notebooks are Galaxy-flavored markdown documents attached to histories. They let users and AI agents co-author living analysis narratives alongside the data that produced them. Every history can have multiple notebooks. Each notebook supports an AI assistant that can read history contents and propose targeted edits.

The key architectural decision: History Notebooks are not a new model. They are regular Galaxy Pages (renamed Reports in the UI Here) with an optional history_id foreign key. This unified approach means history-attached and standalone pages share the same editor, revision system, AI chat, and API surface. The difference is contextual -- history notebooks gain access to history-aware AI tools and are accessed through the history panel.

Three Modes, One Document

History Notebooks serve three distinct usage modes through the same infrastructure:

Human solo in the UI. A researcher running a ChIP-seq pipeline opens the history panel, clicks "Notebooks," and gets a markdown editor pre-titled with the history name. They type prose, drag datasets from the history panel into the editor (auto-inserting Galaxy markdown directives), and preview rendered content with live dataset embeds. This is the computational lab notebook -- no agent required. The researcher documents as they go, building a narrative tied to real artifacts with full revision history.

Human + agent in the UI. The same researcher toggles the chat panel and asks the AI: "Summarize what's in this history and draft a Methods section." The agent inspects the history -- listing datasets, reading metadata, peeking at content -- and proposes a section-level edit. The user sees a per-section diff with checkboxes, accepts the Methods section, rejects the Introduction rewrite. The accepted content creates a new immutable revision tagged edit_source="agent". This is collaborative authoring: the agent drafts, the human curates.

External agent via API. An agent running outside Galaxy -- Claude Code connected via MCP, a custom script, a CI pipeline -- drives an analysis through Galaxy's API and writes to the history notebook programmatically via the same /api/pages endpoints. The notebook becomes the agent's structured output artifact: richer than a chat transcript, tied to real data, reviewable by humans after the fact. A user might prompt: "Analyze these datasets like paper X, keep your results documented in a history notebook." The agent runs tools, updates the notebook iteratively, and the user returns to find not just results but a documented narrative of how they were produced.

All three modes produce revisions in the same append-only log. edit_source on every revision tells you whether a human, an in-app agent, or an external agent wrote it -- provenance that matters for reproducibility and audit.

What's Under the Hood

The implementation spans the full stack:

  • Data model: Page.history_id (nullable FK), PageRevision.edit_source (user/agent/restore provenance), ChatExchange.page_id (page-scoped conversations)
  • AI agent: 5 history-aware tools (list datasets, get metadata, peek content, inspect collections, resolve HIDs to directive arguments), structured output supporting full-document replacement, section-level patches, or conversational responses
  • Frontend: Unified PageEditorView adapting to history vs. standalone context, resizable editor/chat split, revision browser with three diff modes, drag-and-drop from history panel, Window Manager integration
  • Content pipeline: Dual-field API (content for rendering, content_editor for editing) avoiding round-trip encoding problems
  • Test coverage: 148+ tests across Selenium E2E, API integration, agent unit, history tools, chat manager, and Vitest component tests

What It Means

The Computational Lab Notebook Galaxy Never Had

Before any agent enters the picture, History Notebooks fill a basic gap: Galaxy has never had a place to document why alongside what. Researchers have always been able to annotate individual datasets, but there was no way to write a coherent narrative connecting inputs to conclusions within the history itself. History Notebooks give every analysis a living document -- with embedded dataset views, interactive charts, and job parameter tables -- that persists and versions alongside the data.

Agents Need Structured Output, Not Just Chat

When an AI agent analyzes data in Galaxy -- whether through the in-app chat or an external tool like Claude Code -- the valuable output isn't the chat transcript. It's a structured, versioned document with embedded references to real artifacts. History Notebooks give agents (internal and external) a place to build up understanding iteratively, with every revision tracked and attributable. The agent doesn't just answer questions; it maintains a living document accessible through the same UI the human uses.

Reproducibility of Communication, Not Just Computation

Galaxy has long made analyses reproducible through workflows. History Notebooks make the communication of analyses reproducible. The narrative -- why parameters were chosen, which results matter, how to interpret the outputs -- persists alongside the data, versioned and shareable. This applies whether a human wrote the narrative by hand, co-authored it with the in-app agent, or received it from an external agent that documented its own work. The edit_source provenance on each revision makes attribution unambiguous.

The Document-First Paradigm

We deliberately chose documents over Jupyter-style notebooks. Galaxy already handles reproducible, auditable execution. History Notebooks handle documentation and reasoning. Neither tries to be the other. This separation matters for clinical and regulatory settings where validated execution pipelines must be distinct from interactive exploration -- and where audit trails demand clear provenance of who (or what) wrote each revision.

Foundation for Richer Agent Workflows

The unified API surface means any agent that can make HTTP requests can create and update history notebooks. The architecture positions History Notebooks as a common medium across all three usage modes -- human solo, human-AI collaborative, and fully agentic. The pattern scales naturally to richer scenarios: agents that maintain and update notebooks as analyses evolve, embed visualizations alongside prose, and eventually enable workflow extraction where the narrative becomes a reproducible report template.

Current Status

The implementation is feature-complete on the history_pages branch (#21475). The original plan proposed separate HistoryNotebook models and HID-based syntax; the final implementation is architecturally cleaner -- a unified Page model with contextual behavior, eliminating ~630 lines of HID resolution machinery in favor of agent-mediated translation. Remaining work includes streaming agent responses, CodeMirror 6 integration for inline diff, and orchestrator-level agent integration.

The Bigger Picture

If agents aren't strongly encouraged to do science in a reproducible way, the democratization of data analysis will produce more tangles of bash scripts and more results without provenance. The tools we've built to encourage humans to do reproducible data analysis -- structured workflows, versioned artifacts, rich metadata -- are exactly the tools we need for agent-assisted science. History Notebooks are a concrete step: giving agents a structured, versioned, history-coupled medium to document their work, subject to human review, with every edit attributed and every revision preserved.

Architecture

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):

Aspect Reports (standalone) Galaxy Notebooks (history-attached)
Entry point Grid list (/pages/list) or direct URL History panel "Galaxy Notebooks" button
Route /pages/editor?id=X /histories/:historyId/pages/:pageId
history_id null Set — scopes notebook to a history
AI chat tools Text editing only (no history tools) Full history tools: list_history_datasets, get_dataset_info, get_dataset_peek, get_collection_structure, resolve_hid
Drag-and-drop From toolbox directives Also from history panel (datasets/collections)
Permissions modal Yes (ObjectPermissionsModal) No — inherits from history sharing
Save & View Yes (slug-based published URL) No (history context, no slug)
List Grid (/pages/list) Inline HistoryPageList within history panel
Window Manager "View in Window" grid action Click from list opens in WinBox
Auto-create No resolveCurrentPage() 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."

  • Opens history panel -> clicks "Galaxy Notebooks" button
  • System auto-creates a notebook titled after the history
  • Types markdown prose; uses the toolbox to insert dataset references (history_dataset_display(...))
  • Drags a dataset from the history panel into the editor -- directive auto-inserted
  • Clicks Preview to see rendered markdown with live dataset embeds
  • Saves; revision Support different authentication methods #1 recorded with edit_source="user"

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."

  • Opens notebook -> toggles chat panel (split view: 60% editor / 40% chat)
  • Types: "Summarize the datasets in this history and draft a Methods section"
  • Agent calls list_history_datasets -> get_dataset_info -> resolve_hid tools
  • Agent returns a section_patch proposal targeting ## Methods
  • User sees a per-section diff with checkboxes; accepts the Methods section, rejects the Introduction rewrite
  • Applied content creates revision with edit_source="agent"
  • Conversation persists across panel close/reopen and page refresh

Sharing and publishing

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

  • Shared histories expose their notebooks in read-only display mode to other users
  • Reports use the Permissions modal to manage sharing, publishing, and slug assignment

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."

  • Opens notebook -> toggles chat panel
  • Clicks chat history icon to open PageChatHistoryList
  • Browses past conversations with timestamps
  • Selects a previous exchange to resume it
  • Can also delete old conversations to keep things tidy

Revision history and rollback

"The agent's last edit broke my formatting. I want to go back to the previous version."

  • Opens revision panel (right sidebar, 300px)
  • Sees revision list with timestamps and source badges: "Manual", "AI", "Restored"
  • Clicks an old revision to preview it read-only
  • Can compare revision against current content or previous revision (three view modes)
  • Clicks "Restore" -> creates a new revision from old content (edit_source="restore")

4. Architecture Overview

+---------------------------------------------------------------------------+
|                          Frontend (Vue 3)                                  |
|                                                                           |
|  HistoryCounter --> HistoryPageView --> PageEditorView <-- PageEditor      |
|       |                 |    |              |                  |           |
|       |           +-----+    +-----+   MarkdownEditor     (report          |
|       |           |                |    TextEditor          entry)         |
|       |     HistoryPageList        |    (drag-drop)                       |
|       |                      EditorSplitView                              |
|       |                            |                                      |
|       |   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)

Column Type Notes
id int PK
user_id int FK -> galaxy_user Indexed
history_id int FK -> history Nullable, indexed. When set, page is a Galaxy Notebook
title text Not versioned
slug text Indexed. Reports only
latest_revision_id int FK -> page_revision Eager-loaded; circular FK with use_alter
source_invocation_id int FK -> workflow_invocation Nullable. Tracks "generated from invocation"
published / importable bool Report sharing features
deleted bool Soft-delete pattern
create_time / update_time datetime

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

PageRevision (extended)

Column Type Notes
id int PK
page_id int FK -> page Indexed
title text Snapshot of title at revision time
content text Raw markdown with internal IDs
content_format varchar(32) "markdown" or "html"
edit_source varchar(16) New. "user", "agent", or "restore"
create_time / update_time datetime

ChatExchange (extended)

Column Type Notes
page_id int FK -> page Nullable, indexed. Scopes chat to a page

The page_id FK scopes chat exchanges to a page.

Migration

Migration Purpose
b75f0f4dbcd4 Add history_id to page, edit_source to page_revision, page_id to chat_exchange

6. Content Pipeline

Page content flows through two representations:

User edits markdown in MarkdownEditor / TextEditor
        |
        v
  +-------------------+
  |  Raw content       |  Stored in DB as-is
  |  (internal IDs)    |  history_dataset_id=42
  +--------+----------+
           |  rewrite_content_for_export()
           v
  +-----------------------+     +----------------------------+
  |  content_editor        |     |  content                    |
  |  (raw, for editor)     |     |  (encoded IDs + expanded    |
  |  Same as DB content    |     |   directives, for render)   |
  +-----------------------+     +----------------------------+

The API returns both fields in PageDetails:

  • content_editor: What the text editor displays and saves back
  • content: What the Markdown renderer uses (with encoded IDs the existing Galaxy markdown components expect)

This dual-field pattern avoids the round-trip problems that would arise from encoding/decoding IDs on every save cycle.


7. Agent Architecture

PageAssistantAgent

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

Tools (5):

Tool Purpose Returns
list_history_datasets Paginated history item listing HID, name, type, state, size, internal ID
get_dataset_info Detailed metadata for one HID Name, format, state, size, tool info, metadata
get_dataset_peek Pre-computed content preview First lines of dataset content
get_collection_structure Collection element listing Element names, types, states
resolve_hid HID -> directive argument conversion history_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):

Type When Used Content
FullReplacementEdit Complete document rewrite Full new markdown document
SectionPatchEdit Targeted heading-level edit Target heading + new section content
str (plain text) Conversational response No 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)
  +- EditorSplitView (resizable 60/40 split -- 110 lines)
  |    +- 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:

Feature Galaxy Notebook (historyId set) Report (standalone)
Back button target /histories/:hid/pages /pages/list
Title display History name (read-only header) Inline ClickToEdit
Revisions button Always Always
Chat button When agents configured When agents configured
Permissions button Hidden Shown
Save & View Hidden Shown
Preview Navigates with displayOnly=true Opens 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):

  • "preview" — read-only render of revision content
  • "changes_current" — diff against current page content
  • "changes_previous" — diff against previous revision

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:

  • Page list, current page, editor content (raw), title
  • Dirty tracking: isDirty = currentContent !== originalContent || currentTitle !== originalTitle
  • Revision list, selected revision, and revisionViewMode: "preview" | "changes_current" | "changes_previous"
  • UI toggles: showRevisions, showChatPanel (mutually exclusive)
  • Chat history: pageChatHistory, isLoadingChatHistory, showChatHistory, chatHistoryError
  • Loading/saving flags

Cross-session persistence (userLocalStorage):

  • currentPageIds -- remembers which page was open per-history
  • currentChatExchangeIds -- remembers chat exchange per-page
  • dismissedChatProposals -- remembers dismissed proposals per-page

Smart defaults:

  • resolveCurrentPage(historyId) returns stored page, falls back to most recent by update_time, or auto-creates a new one

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

  • History mode: guard checks require historyId for load operations
  • Report mode: savePage() defaults edit_source to "user" if not specified
  • API calls are identical -- unified /api/pages endpoints handle both via optional history_id

Diff System (sectionDiffUtils.ts -- 218 lines)

Built on jsdiff (diff@^8.0.3).

Function Purpose
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

Path Component Notes
/histories/:historyId/pages HistoryPageView Notebook list
/histories/:historyId/pages/:pageId HistoryPageView Notebook edit
/histories/:historyId/pages/:pageId?displayOnly=true HistoryPageView Notebook read-only (WM)
/pages/editor?id=X PageEditor Report edit
/pages/editor?id=X&displayOnly=true PageEditor Report display
/pages/list GridPage Reports grid
/published/page?id=X PageView Published report view

Drag-and-Drop

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

  • Uses Galaxy's eventStore.getDragItems() infrastructure
  • Datasets -> history_dataset_display(history_dataset_id=...) directive
  • Collections -> history_dataset_collection_display(history_dataset_collection_id=...) directive
  • Visual feedback: green dashed border on valid dragover

Window Manager Integration

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

  • Notebooks: Clicking a notebook in HistoryPageList opens it in a WinBox window via displayOnly=true
  • Reports: "View in Window" grid action calls Galaxy.frame.add() with embed URL
  • HistoryCounter: The notebook button respects WM state -- opens in frame when active
  • onUnmounted skips store.$reset() in display mode (iframe independence)

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

Method Path Purpose
GET /api/pages List pages (supports history_id filter)
POST /api/pages Create 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}/undelete Restore

Revisions

Method Path Purpose
GET /api/pages/{id}/revisions List revisions
GET /api/pages/{id}/revisions/{rid} Get revision content
POST /api/pages/{id}/revisions/{rid}/revert Restore to revision (edit_source="restore")

Sharing & Publishing (reports only)

Method Path Purpose
GET /api/pages/{id}/sharing Current sharing status
PUT /api/pages/{id}/enable_link_access Enable link sharing
PUT /api/pages/{id}/publish Publish page
PUT /api/pages/{id}/share_with_users Share with specific users
PUT /api/pages/{id}/slug Set URL slug

Chat

Method Path Purpose
POST /api/chat Send message (with page_id + agent_type)
GET /api/chat/page/{page_id}/history Retrieve page chat history

Index Query Parameters

Param Default Notes
history_id null Filter by history (the key filter for notebooks)
show_own true Show user's own pages
show_published true Show published pages
show_shared false Show pages shared with user
search null Freetext search
sort_by -- create_time, title, update_time, username
limit / offset 100 / 0 Pagination

10. Test Coverage

Summary

Layer Tests LOC Coverage
Selenium E2E 30 669 Navigation, editing, drag-drop, WM, revisions, rename, chat, permissions
API integration 30 336 CRUD, revisions, permissions, chat persistence
Agent unit 39 810 Structured output, tools, prompt injection, history context, live LLM
History tools 32 511 All 5 tool functions
Chat manager 8 149 Page-scoped persistence, filtering
Vitest (components) 11 test files ~3,673 All PageEditor components, store, diff utils

Frontend Test Files

File Lines Focus
pageEditorStore.test.ts 1,422 Store: CRUD, revisions, persistence, report + notebook modes, chat history
PageEditorView.test.ts 649 Unified editor: report + notebook modes, revisions, WM
HistoryPageView.test.ts 367 Notebook list/display/edit routing, lifecycle, WM integration
PageChatPanel.test.ts 385 Chat loading, proposals, feedback, staleness
sectionDiffUtils.test.ts 265 Section parsing, diff computation, patch application
PageRevisionList.test.ts 207 Revision list rendering, source labels, restore
HistoryPageList.test.ts 185 Notebook list, create/select/view events
SectionPatchView.test.ts 68 Section-level patch UI
ProposalDiffView.test.ts 66 Full-replacement diff rendering
PageRevisionView.test.ts 199 Revision preview + diff modes
PageChatHistoryList.test.ts 229 Chat history list rendering, selection, deletion
EditorSplitView.test.ts 59 Resizable split layout

Test Infrastructure

  • Selenium helpers: 13 methods on NavigatesGalaxy (navigate, create, edit, save, rename, revisions, chat)
  • Navigation YAML: 29+ selectors under pages.history section
  • Vitest: Pinia testing utilities, MSW for HTTP mocking, Vue shallowMount
  • Agent tests: Mocked pydantic-ai agent + optional live LLM tests (env-gated)

11. ChatGXY Extraction

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

Component Lines Purpose
ChatMessageCell.vue 345 Message rendering with role styling, feedback buttons, action suggestions
ChatInput.vue 96 Textarea + send button with busy state
ActionCard.vue 97 Action suggestion cards with priority-based styling
agentTypes.ts 60 Agent type registry with icons and labels
chatTypes.ts 26 Shared ChatMessage interface
chatUtils.ts 13 generateId() 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.

  • page.history_id (nullable FK to history) distinguishes notebooks from reports
  • page_revision.edit_source tracks revision provenance ("user", "agent", "restore")
  • chat_exchange.page_id scopes chat to a page
  • All page operations go through the unified /api/pages endpoints with optional history_id filter

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:

  • PageEditorView.vue adapts via mode: "history" | "standalone" (notebook vs report)
  • HistoryPageView.vue handles only list + display routing; edit mode delegates to PageEditorView
  • Legacy PageEditorMarkdown.vue and PageEditor/services.js deleted
  • Single pageEditorStore handles both modes with minimal branching

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.

How to test the changes?

(Select all options that apply)

License

  • I agree to license these and all my past contributions to the core galaxy codebase under the MIT license.

jmchilton and others added 11 commits March 31, 2026 16:24
Merge migration adding history_notebook and history_notebook_revision tables.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1. What Are History Pages?

History Pages 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 pages, and each page supports an AI assistant that can read history contents and propose edits.

History Pages are built on the existing Galaxy **Page** model — they are regular Pages with an optional `history_id` foreign key. This unified model means history-attached and standalone pages share the same editor, revision system, AI chat, and API surface. The difference is contextual: history pages gain access to history-aware AI tools and are accessed through the history panel rather than the pages 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. Standalone Pages vs History Pages

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

| Aspect | Standalone Pages | History Pages |
|--------|-----------------|---------------|
| **Entry point** | Grid list (`/pages/list`) or direct URL | History panel "Pages" button |
| **Route** | `/pages/editor?id=X` | `/histories/:historyId/pages/:pageId` |
| **`history_id`** | null | Set — scopes page to a history |
| **AI chat tools** | Text editing only (no history tools) | Full history tools: `list_history_datasets`, `get_dataset_info`, `get_dataset_peek`, `get_collection_structure`, `resolve_hid` |
| **Drag-and-drop** | From toolbox directives | Also from history panel (datasets/collections) |
| **Permissions modal** | Yes (`ObjectPermissionsModal`) | No — inherits from history sharing |
| **Save & View** | Yes (slug-based published URL) | No (history context, no slug) |
| **Page list** | Grid (`/pages/list`) | Inline `HistoryPageList` within history panel |
| **Window Manager** | "View in Window" grid action | Click from list opens in WinBox |
| **Auto-create** | No | `resolveCurrentPage()` 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."*

- Opens history panel -> clicks "Pages" button
- System auto-creates a page titled after the history
- Types markdown prose; uses the toolbox to insert dataset references (`history_dataset_display(...)`)
- Drags a dataset from the history panel into the editor -- directive auto-inserted
- Clicks Preview to see rendered markdown with live dataset embeds
- Saves; revision #1 recorded with `edit_source="user"`

AI-assisted page 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."*

- Opens page -> toggles chat panel (split view: 60% editor / 40% chat)
- Types: "Summarize the datasets in this history and draft a Methods section"
- Agent calls `list_history_datasets` -> `get_dataset_info` -> `resolve_hid` tools
- Agent returns a `section_patch` proposal targeting `## Methods`
- User sees a per-section diff with checkboxes; accepts the Methods section, rejects the Introduction rewrite
- Applied content creates revision with `edit_source="agent"`
- Conversation persists across panel close/reopen and page refresh

Sharing and publishing

> *"My analysis is complete. I want to share the page read-only."*

- Shared histories expose their pages in read-only display mode to other users
- Standalone pages use the Permissions modal to manage sharing, publishing, and slug assignment

Revision history and rollback

> *"The agent's last edit broke my formatting. I want to go back."*

- Opens revision panel (right sidebar, 300px)
- Sees revision list with timestamps and source badges: "Manual", "AI", "Restored"
- Clicks an old revision to preview it read-only
- Clicks "Restore" -> creates a new revision from old content (`edit_source="restore"`)

---

4. Architecture Overview

```
+---------------------------------------------------------------------------+
|                          Frontend (Vue 3)                                  |
|                                                                           |
|  HistoryCounter --> HistoryPageView --> PageEditorView <-- PageEditor      |
|       |                 |    |              |                  |           |
|       |           +-----+    +-----+   MarkdownEditor     (standalone     |
|       |           |                |    TextEditor          entry)         |
|       |     HistoryPageList        |    (drag-drop)                       |
|       |                      EditorSplitView                              |
|       |                            |                                      |
|       |   PageRevisionList    PageChatPanel                               |
|       |   PageRevisionView     |         |                                |
|       |                  ChatMessageCell  ProposalDiffView                 |
|       |                  ChatInput       SectionPatchView                  |
|       |                                                                   |
|  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)

| Column | Type | Notes |
|--------|------|-------|
| `id` | int PK | |
| `user_id` | int FK -> galaxy_user | Indexed |
| `history_id` | int FK -> history | **Nullable**, indexed. When set, page is history-attached |
| `title` | text | Not versioned |
| `slug` | text | Indexed. Standalone pages only |
| `latest_revision_id` | int FK -> page_revision | Eager-loaded; circular FK with `use_alter` |
| `source_invocation_id` | int FK -> workflow_invocation | Nullable. Tracks "generated from invocation" |
| `published` / `importable` | bool | Standalone sharing features |
| `deleted` | bool | Soft-delete pattern |
| `create_time` / `update_time` | datetime | |

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

PageRevision (extended)

| Column | Type | Notes |
|--------|------|-------|
| `id` | int PK | |
| `page_id` | int FK -> page | Indexed |
| `title` | text | Snapshot of title at revision time |
| `content` | text | Raw markdown with internal IDs |
| `content_format` | varchar(32) | `"markdown"` or `"html"` |
| `edit_source` | varchar(16) | **New.** `"user"`, `"agent"`, or `"restore"` |
| `create_time` / `update_time` | datetime | |

ChatExchange (extended)

| Column | Type | Notes |
|--------|------|-------|
| `page_id` | int FK -> page | Nullable, indexed. Scopes chat to a page |

The original `notebook_id` FK was replaced with `page_id` when the HistoryNotebook model was merged into Page.

6. Content Pipeline

Page content flows through two representations:

```
User edits markdown in MarkdownEditor / TextEditor
        |
        v
  +-------------------+
  |  Raw content       |  Stored in DB as-is
  |  (internal IDs)    |  history_dataset_id=42
  +--------+----------+
           |  rewrite_content_for_export()
           v
  +-----------------------+     +----------------------------+
  |  content_editor        |     |  content                    |
  |  (raw, for editor)     |     |  (encoded IDs + expanded    |
  |  Same as DB content    |     |   directives, for render)   |
  +-----------------------+     +----------------------------+
```

The API returns **both** fields in `PageDetails`:
- `content_editor`: What the text editor displays and saves back
- `content`: What the Markdown renderer uses (with encoded IDs the existing Galaxy markdown components expect)

This dual-field pattern avoids the round-trip problems that would arise from encoding/decoding IDs on every save cycle.

---

7. Agent Architecture

PageAssistantAgent

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

**Tools (5):**

| Tool | Purpose | Returns |
|------|---------|---------|
| `list_history_datasets` | Paginated history item listing | HID, name, type, state, size, internal ID |
| `get_dataset_info` | Detailed metadata for one HID | Name, format, state, size, tool info, metadata |
| `get_dataset_peek` | Pre-computed content preview | First lines of dataset content |
| `get_collection_structure` | Collection element listing | Element names, types, states |
| `resolve_hid` | HID -> directive argument conversion | `history_dataset_id=N` or `history_dataset_collection_id=N` + `job_id` |

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

**Output types (3, discriminated by `mode` literal):**

| Type | When Used | Content |
|------|-----------|---------|
| `FullReplacementEdit` | Complete document rewrite | Full new markdown document |
| `SectionPatchEdit` | Targeted heading-level edit | Target heading + new section content |
| `str` (plain text) | Conversational response | No 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 history-attached) 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

---

8. Frontend Components

Component Tree

```
History-attached entry:
  HistoryCounter (button in history panel)
    +- HistoryPageView (list + display routing -- 176 lines)
         +- HistoryPageList (page picker -- 89 lines)
         +- Markdown (display-only render)
         +- PageEditorView (edit mode delegation)

Standalone entry:
  PageEditor (thin wrapper -- 13 lines)
    +- PageEditorView

PageEditorView (unified editor -- 364 lines)
  +- ClickToEdit (inline title editing)
  +- MarkdownEditor
  |    +- TextEditor (drag-and-drop for history items)
  +- PageRevisionList (sidebar panel -- 88 lines)
  +- PageRevisionView (read-only revision preview -- 59 lines)
  +- EditorSplitView (resizable 60/40 split -- 111 lines)
  |    +- PageChatPanel (agent chat -- 477 lines)
  |         +- ChatMessageCell (shared from ChatGXY)
  |         +- ChatInput (shared from ChatGXY)
  |         +- ProposalDiffView (full-doc diff -- 123 lines)
  |         +- SectionPatchView (per-section diff -- 207 lines)
  +- ObjectPermissionsModal (standalone only -- 16 lines)
  |    +- ObjectPermissions (344 lines)
```

PageEditorView (unified editor)

The core editor component. Adapts based on context:

| Feature | `historyId` set | standalone |
|---------|----------------|------------|
| Back button target | `/histories/:hid/pages` | `/pages/list` |
| Title display | History name (read-only header) | Inline `ClickToEdit` |
| Revisions button | Always | Always |
| Chat button | When agents configured | When agents configured |
| Permissions button | Hidden | Shown |
| Save & View | Hidden | Shown |
| Preview | Navigates with `displayOnly=true` | Opens 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 (full-page revision preview with Restore button)
5. Edit mode (toolbar + editor + optional chat/revision sidepanels)

HistoryPageView (history context router)

Routes between three states for history-attached pages:
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 page in the list opens it in a WinBox window via `displayOnly=true` with `router.push(url, { title, preventWindowManager: false })`.

Pinia Store (`pageEditorStore` -- 472 lines)

**Mode:** `mode: "history" | "standalone"` -- controls which features are available.

**State management:**
- Page list, current page, editor content (raw), title
- Dirty tracking: `isDirty = currentContent !== originalContent || currentTitle !== originalTitle`
- Revision list and selected revision
- UI toggles: `showRevisions`, `showChatPanel` (mutually exclusive)
- Loading/saving flags

**Cross-session persistence (userLocalStorage):**
- `currentPageIds` -- remembers which page was open per-history
- `currentChatExchangeIds` -- remembers chat exchange per-page
- `dismissedChatProposals` -- remembers dismissed proposals per-page

**Smart defaults:**
- `resolveCurrentPage(historyId)` returns stored page, falls back to most recent by update_time, or auto-creates a new one

**Mode differentiation is minimal** -- mostly a UI/UX signal:
- History mode: guard checks require `historyId` for load operations
- Standalone mode: `savePage()` defaults `edit_source` to `"user"` if not specified
- API calls are identical -- unified `/api/pages` endpoints handle both via optional `history_id`

Diff System (`sectionDiffUtils.ts` -- 218 lines)

Built on [jsdiff](https://github.com/kpdecker/jsdiff) (`diff@^8.0.3`).

| Function | Purpose |
|----------|---------|
| `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

| Path | Component | Notes |
|------|-----------|-------|
| `/histories/:historyId/pages` | HistoryPageView | List mode |
| `/histories/:historyId/pages/:pageId` | HistoryPageView | Edit mode |
| `/histories/:historyId/pages/:pageId?displayOnly=true` | HistoryPageView | Read-only rendered (WM) |
| `/pages/editor?id=X` | PageEditor | Standalone edit |
| `/pages/editor?id=X&displayOnly=true` | PageEditor | Standalone display |
| `/pages/list` | GridPage | Standalone page grid |
| `/published/page?id=X` | PageView | Published/embed view |

Drag-and-Drop

TextEditor supports drag from the history panel when `mode="page"`:
- Uses Galaxy's `eventStore.getDragItems()` infrastructure
- Datasets -> `history_dataset_display(history_dataset_id=...)` directive
- Collections -> `history_dataset_collection_display(history_dataset_collection_id=...)` directive
- Visual feedback: green dashed border on valid dragover

Window Manager Integration

When Galaxy's Window Manager (WinBox) is active:
- **History pages:** Clicking a page in `HistoryPageList` opens it in a WinBox window via `displayOnly=true`
- **Standalone pages:** "View in Window" grid action calls `Galaxy.frame.add()` with embed URL
- **HistoryCounter:** The page button respects WM state -- opens in frame when active
- `onUnmounted` skips `store.$reset()` in display mode (iframe independence)

---

9. API Surface

All page operations use the unified `/api/pages` endpoints. History-attached pages are just pages with `history_id` set.

Page CRUD

| Method | Path | Purpose |
|--------|------|---------|
| `GET` | `/api/pages` | List pages (supports `history_id` filter) |
| `POST` | `/api/pages` | Create 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}/undelete` | Restore |

Revisions

| Method | Path | Purpose |
|--------|------|---------|
| `GET` | `/api/pages/{id}/revisions` | List revisions |
| `GET` | `/api/pages/{id}/revisions/{rid}` | Get revision content |
| `POST` | `/api/pages/{id}/revisions/{rid}/revert` | Restore to revision (`edit_source="restore"`) |

Sharing & Publishing (standalone pages)

| Method | Path | Purpose |
|--------|------|---------|
| `GET` | `/api/pages/{id}/sharing` | Current sharing status |
| `PUT` | `/api/pages/{id}/enable_link_access` | Enable link sharing |
| `PUT` | `/api/pages/{id}/publish` | Publish page |
| `PUT` | `/api/pages/{id}/share_with_users` | Share with specific users |
| `PUT` | `/api/pages/{id}/slug` | Set URL slug |

Chat

| Method | Path | Purpose |
|--------|------|---------|
| `POST` | `/api/chat` | Send message (with `page_id` + `agent_type`) |
| `GET` | `/api/chat/page/{page_id}/history` | Retrieve page chat history |

Index Query Parameters

| Param | Default | Notes |
|-------|---------|-------|
| `history_id` | null | Filter pages by history (the key filter for history-attached pages) |
| `show_own` | true | Show user's own pages |
| `show_published` | true | Show published pages |
| `show_shared` | false | Show pages shared with user |
| `search` | null | Freetext search |
| `sort_by` | -- | `create_time`, `title`, `update_time`, `username` |
| `limit` / `offset` | 100 / 0 | Pagination |

---

10. Test Coverage

Summary

| Layer | Tests | LOC | Coverage |
|-------|-------|-----|----------|
| Selenium E2E | 24 | 489 | Navigation, editing, drag-drop, WM, revisions, rename |
| API integration | in `test_pages_history_attached.py` | 13,561 | CRUD, revisions, permissions |
| Vitest (components) | 9 test files | 2,241 | All PageEditor components, store, diff utils |
| Agent unit | 28 | 708 | Structured output, tools, prompt injection, live LLM |
| History tools | 32 | 511 | All 5 tool functions |
| Chat manager | 8 | 149 | Page-scoped persistence, filtering |

Frontend Test Files

| File | Lines | Focus |
|------|-------|-------|
| `PageEditorView.test.ts` | 645 | Unified editor: standalone + history modes, revisions, WM |
| `HistoryPageView.test.ts` | 367 | List/display/edit routing, lifecycle, WM integration |
| `PageChatPanel.test.ts` | 379 | Chat loading, proposals, feedback, staleness |
| `sectionDiffUtils.test.ts` | 265 | Section parsing, diff computation, patch application |
| `PageRevisionList.test.ts` | 207 | Revision list rendering, source labels, restore |
| `HistoryPageList.test.ts` | 185 | Page list, create/select/view events |
| `ProposalDiffView.test.ts` | 66 | Full-replacement diff rendering |
| `SectionPatchView.test.ts` | 68 | Section-level patch UI |
| `EditorSplitView.test.ts` | 59 | Resizable split layout |
| `pageEditorStore.test.ts` | 952 | Store: CRUD, revisions, persistence, standalone mode |

Test Infrastructure

- **Selenium helpers:** 11 methods on `NavigatesGalaxy` (navigate, create, edit, save, rename, revisions)
- **Navigation YAML:** 25+ selectors under `pages.history` section
- **Vitest:** Pinia testing utilities, MSW for HTTP mocking, Vue shallowMount
- **Agent tests:** Mocked pydantic-ai agent + optional live LLM tests (env-gated)

---

11. ChatGXY Extraction

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

| Component | Lines | Purpose |
|-----------|-------|---------|
| `ChatMessageCell.vue` | 110 | Message rendering with role styling, feedback buttons, action suggestions |
| `ChatInput.vue` | ~40 | Textarea + send button with busy state |
| `ActionCard.vue` | 80 | Action suggestion cards with priority-based styling |
| `agentTypes.ts` | 59 | Agent type registry with icons and labels |
| `chatTypes.ts` | 15 | Shared `ChatMessage` interface |
| `chatUtils.ts` | 12 | `generateId()` and `scrollToBottom()` helpers |

---

12. Design Decisions

Model Merge: HistoryNotebook -> Page

The original implementation created separate `HistoryNotebook` and `HistoryNotebookRevision` tables. After removing HID syntax (which was the only structural difference between notebooks and pages), the models were identical. The merge:

- Added `page.history_id` (nullable FK to history) instead of a separate table
- Added `page_revision.edit_source` to track revision provenance
- Changed `chat_exchange.notebook_id` -> `chat_exchange.page_id`
- Eliminated separate API endpoints (`/api/histories/{id}/notebooks/*`), manager, and schema classes
- All page operations now go through the unified `/api/pages` endpoints with optional `history_id` filter

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

HID Syntax (Decided: Removed from storage layer)

History pages originally introduced `hid=N` syntax in stored markdown -- ~630 lines across 16 files for backend resolution, dual content fields, client-side provide/inject, and store-based HID-to-ID mapping.

**Current approach:** Pages store `history_dataset_id=X` (matching existing Page syntax). The agent uses `resolve_hid` as a tool to bridge between user-visible HIDs and directive IDs. This eliminates the 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

Two parallel editors existed: legacy `PageEditorMarkdown.vue` (Options API, local state, no revisions/chat) and `HistoryNotebookView.vue` (Composition API, Pinia store, full features). The convergence:

- Created `PageEditorView.vue` as a single editor that adapts via `mode: "history" | "standalone"`
- `HistoryPageView.vue` kept only list + display routing; edit mode delegates to `PageEditorView`
- Legacy `PageEditorMarkdown.vue` and `PageEditor/services.js` deleted
- Single `pageEditorStore` handles both modes with minimal branching

Multiple Pages Per History

No unique constraint on `page.history_id`. A history can have multiple pages 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.
Static agent rules, nav selectors, populator helpers, 5 API tests
(create/history/multi-turn/isolation/delete), 4 Selenium tests
(toggle/greeting/multi-turn/new-conversation).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…encies.

GalaxyAgentDependencies requires get_agent (registry callback) after the
AgentRegistry injection refactor; other test classes already passed it.
This restores mypy call-arg compliance for the page assistant tests.

Made-with: Cursor
restore_revision passed already-internal content through save_new_revision
which re-ran rewrite_content_for_import, double-decoding IDs. Build
PageRevision directly to skip import. Add regression tests for reverting
pages with Galaxy directives (history-attached + invocation reports).

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@guerler
Copy link
Copy Markdown
Contributor

guerler commented Apr 2, 2026

Should we standardize on “Galaxy Notebooks” as the user-facing term and treat the history-attached vs standalone distinction as context?

@jmchilton
Copy link
Copy Markdown
Member Author

@guerler I believe this PR does this and I just built the PR description from older docs - let me double check this though.

@jmchilton jmchilton changed the title History Notebooks: Persistent Narrative for Human-AI Collaborative Science in Galaxy Galaxy Notebooks: Persistent Narrative for Human-AI Collaborative Science in Galaxy Apr 2, 2026
@jmchilton
Copy link
Copy Markdown
Member Author

Nevermind - I misread the question and Claude did not - they pay so much more attention to detail than me. The answer is:

This has NOT been done. The codebase still uses the two-term split — "Galaxy Notebook" for history-attached and "Report" for standalone. The distinction is implemented through PAGE_LABELS in client/src/components/Page/constants.ts with separate label sets per mode.

However, the architecture is already set up to make this change trivial — all user-facing strings flow through PAGE_LABELS and the various *_LABELS constants. To standardize on "Galaxy Notebooks" everywhere, you'd just update the standalone labels in constants.ts plus GRID_LABELS, ACTIVITY_LABELS, PUBLISHED_LABELS, FORM_LABELS, EMBED_LABELS, and PERMISSIONS_LABELS.

I think this reflects the comments Marius made in the slack chat - it sounds like pages is confusing and he calls them Reports when he talks to actual users. The reports language changes here merges the terminology between workflow reports and pages somewhat - while setting Galaxy Notebooks out as a... living document that is different. If we instead called Pages "Galaxy Notebooks" and eliminated that Report terminology - than workflows would be the odd thing out. My two cents is the thing we generate from an invocation is not a notebook - so the term Galaxy Notebook feels wrong to me - but I acknowledge the desire to converge the terminology further or along different axes is all reasonable.

@guerler
Copy link
Copy Markdown
Contributor

guerler commented Apr 2, 2026

That split does make sense to me, Galaxy Notebooks as the primary editable document and Galaxy Reports as invocation-generated starting points. Thank you!

if page_id:
from galaxy.model import Page

page_obj = trans.sa_session.get(Page, page_id)
Copy link
Copy Markdown
Contributor

@guerler guerler Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we check that the user is allowed to access the page?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

Status: Needs Review

Development

Successfully merging this pull request may close these issues.

2 participants