Polish chat-preview rendering of raw Galaxy markdown directives
Summary
Agent chat replies that inline Galaxy directives (e.g. ```galaxy\nhistory_dataset_display(history_dataset_id=42)\n```) render as raw text in the chat panel — because chat uses a vanilla MarkdownIt engine, not the directive-hydrating renderer used by the page preview. Behavior is correct (chat should not hydrate directives), but the unstyled raw text is unpolished. Goal: keep chat non-hydrating, but present directive blocks/inline refs as readable chips/code with light affordances.
Current behavior
- Chat panel renders assistant content via
props.renderMarkdown(props.message.content)and injects HTML withv-html—client/src/components/ChatGXY/ChatMessageCell.vue:51. renderMarkdowncomes fromuseMarkdown({ openLinksInNewPage: true, removeNewlinesAfterList: true })inclient/src/components/PageEditor/PageChatPanel.vue:47.useMarkdownis plain MarkdownIt with link/heading/list rules only — nogalaxyfence handling — seeclient/src/composables/markdown.ts:158.- The page preview path is different:
Markdown.vuecallsparseMarkdownwhich splits```galaxyfences into directive sections and dispatches them throughSectionWrapper—client/src/components/Markdown/Markdown.vue:74,client/src/components/Markdown/parse.ts:47. - Result: a
```galaxyblock in a chat message is rendered by MarkdownIt as a generic<pre><code>with literalhistory_dataset_display(...)inside, and inline${galaxy history_dataset_name(...)}shows up unmodified as text. - Diff renderers are unaffected and intentional:
ProposalDiffView.vue:64andSectionPatchView.vue:127already use raw<pre>for diff lines — diffs of markdown should show the source.
Why this is fine but unpolished
Hydrating directives inside chat would re-trigger dataset fetches per message, drag in heavy components for content the user has not asked to embed, and make scrollback re-render the world. The chat is correctly a transcript, not a published page. The problem is purely cosmetic: raw ```galaxy and ${galaxy ...} look like leaked internals, and embed-heavy proposals copy-pasted into prose can bloat exchanges. Polish, don’t hydrate.
Proposed direction (recommended)
STYLED_CHIPS_AND_FENCE_PILL: keep a non-hydrating MarkdownIt path, but add small post-processing so directive surfaces read as first-class UI:
- For
```galaxyfences: render a labeled code block — pill header “Galaxy directive” + directive name extracted from the first non-blank line, plus the existing monospace body. Optional collapse toggle if body > N lines. - For inline
${galaxy history_dataset_name(history_dataset_id=ID)}: render as a compact chip showing “directive_name (hid=…)” using existing inline<code>styling already present inChatMessageCell.vue:193. - Use
EMBED_CAPABLE_DIRECTIVES/VALID_ARGUMENTSfromlib/galaxy/managers/markdown_parse.pyas the recognition set (mirror in TS, or generate). Unknown directive names render unchanged so we don’t accidentally hide malformed agent output the user needs to see. - Add a per-block “Insert into page” affordance only if cheap — otherwise punt to a follow-up. Primary goal is legibility.
Rationale: reuses the existing parse.ts directive-extraction logic conceptually without depending on its Vue hydration chain; keeps chat fast; matches the agent’s intent (“here is the directive I’d embed”) without lying about live rendering.
Alternatives considered
| Option | One-liner | Why rejected |
|---|---|---|
| STYLED_CHIPS_AND_FENCE_PILL | Post-process MarkdownIt output to chip directives, no hydration. | Recommended. |
| FULL_HYDRATION_IN_CHAT | Run chat content through Markdown.vue / SectionWrapper. | Triggers per-message dataset/job fetches; heavy DOM in transcript; defeats purpose of chat-as-log; risks reactive loops if message list rerenders. |
| PROMPT_ONLY_FIX | Tell agent in page_assistant.md never to emit directives in chat prose, only in replace_entire_document / patch_section. | Necessary-but-not-sufficient — prompt is already strict (lib/galaxy/agents/prompts/page_assistant.md:31, :55) and agent will still cite directives when explaining (“here’s the directive I used: …”). Pair this with STYLED_CHIPS rather than rely on it alone. Worth a small tweak (see Implementation). |
| STRIP_DIRECTIVES_FROM_CHAT | Pre-process to remove ```galaxy blocks from chat content before render. | Hides information from the user; breaks “how does directive X work?” Q&A use case explicitly called out in prompt (:33). |
| HYDRATE_ONLY_NAME_DIRECTIVES | Hydrate only cheap inline directives (history_dataset_name). | Inconsistent surface; still triggers small fetches per chat scroll; not worth complexity over chips. |
Implementation plan
- Add a TS module
client/src/components/ChatGXY/galaxyDirectivePreview.ts(or underMarkdown/) exposing:KNOWN_DIRECTIVES: Set<string>mirroringVALID_ARGUMENTSkeys.formatDirectiveBlock(raw: string): { name, args, body }— reuses logic shape fromclient/src/components/Markdown/parse.ts:151(FUNCTION_CALL_LINE_TEMPLATE) without depending on Vue/SectionWrapper.
- Extend
useMarkdowninclient/src/composables/markdown.tswith an opt-in flaggalaxyDirectivePreview: booleanthat registers a MarkdownIt rule replacingfencerendering forinfo === "galaxy"and a text rule (or post-render walk) for inline${galaxy …}. Output static HTML with classes likechat-galaxy-directive,chat-galaxy-inline-directive. (Note:parse.ts:5definesFUNCTION_CALL_LINE_TEMPLATE;parse.ts:151is the usage site — both worth glancing at.) - Set that flag at the
useMarkdowncall site:client/src/components/PageEditor/PageChatPanel.vue:47. - Add scoped styles for the new classes in
client/src/components/ChatGXY/ChatMessageCell.vue(pill header, collapse, inline chip) — these go in the existing<style scoped>deep-selectors block (:deep(pre)neighborhood at lines 201–213). - Minor prompt nudge in
lib/galaxy/agents/prompts/page_assistant.md: in the chat-vs-proposal section (~:31–:33), explicitly say “when answering questions about directives, name the directive inline (history_dataset_display) rather than pasting a full fenced block unless illustrating syntax”. Keeps the existing strict embed-in-proposal guidance. - Tests:
- Extend
client/src/components/ChatGXY/ChatMessageCell.test.tswith cases: known```galaxyfence renders pill + body, unknown fence info-string renders as plain pre, inline${galaxy history_dataset_name(...)}renders chip, malformed directive falls through to unchanged. - Add a
client/src/composables/markdown.test.jscase for the newgalaxyDirectivePreviewflag. PageChatPanel.test.tsonly needs a smoke assertion that the flag is on; no integration churn expected.- No backend tests — directive parser is unchanged.
- Extend
- No new fixtures required; reuse strings already present in chat tests /
parse.tstest inputs.
Open questions
- Should the chip carry a “copy directive” button, or rely on text-select?
- Generate the TS
KNOWN_DIRECTIVESset frommarkdown_parse.py(build step) or hand-mirror with a CI drift test?
(Implementation-time decisions deferred: collapse threshold, chip-arg detail, resolved-HID vs encoded-ID in chip header.)
References
- galaxyproject/galaxy#22361
client/src/components/ChatGXY/ChatMessageCell.vue:51— chatv-htmlinjection pointclient/src/components/PageEditor/PageChatPanel.vue:47—useMarkdowncall site for chatclient/src/composables/markdown.ts:158— vanilla MarkdownIt, no galaxy fence handlingclient/src/components/Markdown/Markdown.vue:74— preview usesparseMarkdown+SectionWrapperclient/src/components/Markdown/parse.ts:47—```galaxyfence extraction logic to mirrorclient/src/components/PageEditor/ProposalDiffView.vue:64,SectionPatchView.vue:127— diff views, intentionally raw, out of scopelib/galaxy/agents/prompts/page_assistant.md:31,:55— existing chat-vs-proposal guidance + embedding ruleslib/galaxy/managers/markdown_parse.py:26,:71—VALID_ARGUMENTS,EMBED_CAPABLE_DIRECTIVESsource of truth- Recent prompt-tuning commits:
e74becc73f,f866f7f7c9,5d277375cd,4afd63a9a9