Skip to content

An experimental Markdown renderer#597

Open
xenodium wants to merge 20 commits into
mainfrom
inline-markdown
Open

An experimental Markdown renderer#597
xenodium wants to merge 20 commits into
mainfrom
inline-markdown

Conversation

@xenodium
Copy link
Copy Markdown
Owner

@xenodium xenodium commented May 20, 2026

Experimenting with inline text properties to render Markdown text into agent-shell buffers.

agent-shell's Markdown renderer today is powered by overlays. While overlays have served us well for some time, they have some limitations, primarily around performance (so far has been good enough-ish) but also with text navigation/selection.

This branch is an experiment to see if we can achieve an improved experience around Markdown rendering using inlined text properties (no overlays).

Two initial areas of focus:

Table navigation and content selection (not currently possible in today's overlay implementaion)

2026-05-20-19:13:40-Emacs_optimized

Cell navigation

  • M-x agent-shell-markdown-table-next-cell
  • M-x agent-shell-markdown-table-previous-cell

More performant code block rendering

image

Enabling experimental renderer

To try out the experimental renderer in this branch use:

(setq agent-shell--experimental-renderer t)

Report bugs

This is fairly experimental, so please do report bugs.

@mplanchard
Copy link
Copy Markdown

Seems work nicely! If your lazy-highlight face is less distracting than the gray one that you have, it looks a bit much:

image

I'm using the modus-vivendi-deuteranopia theme. Easy enough to change this particular face, but it would be nice to have a built in option to not highlight it or something.

@mplanchard
Copy link
Copy Markdown

Oh yeah it also seems like I'm not getting the fancy table behavior described, it's just showing the markdown table.

@mplanchard
Copy link
Copy Markdown

Oh yeah it also seems like I'm not getting the fancy table behavior described, it's just showing the markdown table.

Ah, it seems to work with claude, but not codex, is the thing

@xenodium
Copy link
Copy Markdown
Owner Author

Oh yeah it also seems like I'm not getting the fancy table behavior described

@mplanchard that's because the LLM embedded it in a code block. Ask it to give you the table without a code fences.

@mplanchard
Copy link
Copy Markdown

@mplanchard that's because the LLM embedded it in a code block. Ask it to give you the table without a code fences.

eyyy yeah that does it, although the heading alignment is a bit funky

image

@xenodium
Copy link
Copy Markdown
Owner Author

Ah thanks. I'll need to see the traffic that generated the Markdown to look into it. https://github.com/xenodium/agent-shell?tab=readme-ov-file#how-do-i-viewget-agent-client-protocol-traffic

@xenodium
Copy link
Copy Markdown
Owner Author

If your lazy-highlight face is less distracting than the gray one that you have, it looks a bit much

Good to know. I'll see if I can find a more subtle default. Having said that, the new renderer offers faces that can be overriden.

@mplanchard
Copy link
Copy Markdown

Ah thanks. I'll need to see the traffic that generated the Markdown to look into it. https://github.com/xenodium/agent-shell?tab=readme-ov-file#how-do-i-viewget-agent-client-protocol-traffic

Roger, here you go

traffic output
13:14:13.690 → request      session/list
13:14:13.692 → request      session/prompt
13:14:13.754 ← response     result
13:14:15.454 ← notification session/update
13:14:15.455 ← notification session/update
13:14:15.482 ← notification session/update
13:14:15.483 ← notification session/update
13:14:15.514 ← notification session/update
13:14:15.515 ← notification session/update
13:14:15.548 ← notification session/update
13:14:15.572 ← notification session/update
13:14:15.572 ← notification session/update
13:14:15.593 ← notification session/update
13:14:15.594 ← notification session/update
13:14:15.620 ← notification session/update
13:14:15.639 ← notification session/update
13:14:15.658 ← notification session/update
13:14:15.677 ← notification session/update
13:14:15.724 ← notification session/update
13:14:15.748 ← notification session/update
13:14:15.749 ← notification session/update
13:14:15.770 ← notification session/update
13:14:15.770 ← notification session/update
13:14:15.796 ← notification session/update
13:14:15.797 ← notification session/update
13:14:15.838 ← notification session/update
13:14:15.856 ← notification session/update
13:14:15.875 ← notification session/update
13:14:15.876 ← notification session/update
13:14:15.897 ← notification session/update
13:14:15.918 ← notification session/update
13:14:15.938 ← notification session/update
13:14:15.958 ← notification session/update
13:14:15.977 ← notification session/update
13:14:15.996 ← notification session/update
13:14:16.112 ← notification session/update
13:14:16.113 ← notification session/update
13:14:16.115 ← notification session/update
13:14:16.118 ← notification session/update
13:14:16.120 ← notification session/update
13:14:16.123 ← notification session/update
13:14:16.149 ← notification session/update
13:14:16.149 ← notification session/update
13:14:16.182 ← notification session/update
13:14:16.221 ← notification session/update
13:14:16.222 ← notification session/update
13:14:16.224 ← notification session/update
13:14:16.227 ← notification session/update
13:14:16.254 ← notification session/update
13:14:16.328 ← notification session/update
13:14:16.329 ← notification session/update
13:14:16.332 ← notification session/update
13:14:16.334 ← notification session/update
13:14:16.361 ← notification session/update
13:14:16.362 ← notification session/update
13:14:16.365 ← notification session/update
13:14:16.392 ← notification session/update
13:14:16.419 ← notification session/update
13:14:16.420 ← notification session/update
13:14:16.449 ← notification session/update
13:14:16.473 ← notification session/update
13:14:16.474 ← notification session/update
13:14:16.514 ← notification session/update
13:14:16.515 ← notification session/update
13:14:16.551 ← notification session/update
13:14:16.552 ← notification session/update
13:14:16.585 ← notification session/update
13:14:16.587 ← notification session/update
13:14:16.619 ← notification session/update
13:14:16.620 ← notification session/update
13:14:16.657 ← notification session/update
13:14:16.659 ← notification session/update
13:14:16.682 ← notification session/update
13:14:16.683 ← notification session/update
13:14:16.713 ← notification session/update
13:14:16.713 ← notification session/update
13:14:16.748 ← notification session/update
13:14:16.773 ← notification session/update
13:14:16.793 ← notification session/update
13:14:16.935 ← notification session/update
13:14:16.936 ← notification session/update
13:14:16.948 ← notification session/update
13:14:16.952 ← notification session/update
13:14:16.956 ← notification session/update
13:14:16.960 ← notification session/update
13:14:16.963 ← notification session/update
13:14:16.967 ← notification session/update
13:14:16.996 ← notification session/update
13:14:16.997 ← notification session/update
13:14:17.000 ← notification session/update
13:14:17.037 ← notification session/update
13:14:17.037 ← notification session/update
13:14:17.041 ← notification session/update
13:14:17.045 ← notification session/update
13:14:17.068 ← notification session/update
13:14:17.090 ← notification session/update
13:14:17.112 ← notification session/update
13:14:17.114 ← notification session/update
13:14:17.144 ← notification session/update
13:14:17.173 ← notification session/update
13:14:17.175 ← notification session/update
13:14:17.217 ← notification session/update
13:14:17.258 ← notification session/update
13:14:17.286 ← notification session/update
13:14:17.287 ← notification session/update
13:14:17.397 ← notification session/update
13:14:17.403 ← notification session/update
13:14:17.430 ← notification session/update
13:14:17.431 ← notification session/update
13:14:17.435 ← notification session/update
13:14:17.439 ← notification session/update
13:14:17.443 ← notification session/update
13:14:17.447 ← notification session/update
13:14:17.451 ← notification session/update
13:14:17.455 ← notification session/update
13:14:17.491 ← notification session/update
13:14:17.492 ← notification session/update
13:14:17.495 ← notification session/update
13:14:17.526 ← notification session/update
13:14:17.528 ← notification session/update
13:14:17.532 ← notification session/update
13:14:17.557 ← notification session/update
13:14:17.559 ← notification session/update
13:14:17.583 ← notification session/update
13:14:17.715 ← notification session/update
13:14:17.717 ← notification session/update
13:14:17.722 ← notification session/update
13:14:17.725 ← notification session/update
13:14:17.729 ← notification session/update
13:14:17.767 ← notification session/update
13:14:17.768 ← notification session/update
13:14:17.772 ← notification session/update
13:14:17.777 ← notification session/update
13:14:17.781 ← notification session/update
13:14:17.813 ← notification session/update
13:14:17.815 ← notification session/update
13:14:17.840 ← notification session/update
13:14:17.864 ← notification session/update
13:14:17.866 ← notification session/update
13:14:17.870 ← notification session/update
13:14:17.915 ← notification session/update
13:14:17.916 ← notification session/update
13:14:17.947 ← notification session/update
13:14:17.948 ← notification session/update
13:14:18.087 ← notification session/update
13:14:18.089 ← notification session/update
13:14:18.094 ← notification session/update
13:14:18.100 ← notification session/update
13:14:18.105 ← notification session/update
13:14:18.109 ← notification session/update
13:14:18.114 ← notification session/update
13:14:18.119 ← notification session/update
13:14:18.152 ← notification session/update
13:14:18.153 ← notification session/update
13:14:18.156 ← notification session/update
13:14:18.195 ← notification session/update
13:14:18.196 ← notification session/update
13:14:18.224 ← notification session/update
13:14:18.226 ← notification session/update
13:14:18.231 ← notification session/update
13:14:18.384 ← notification session/update
13:14:18.387 ← notification session/update
13:14:18.392 ← notification session/update
13:14:18.398 ← notification session/update
13:14:18.403 ← notification session/update
13:14:18.469 ← response     result
13:14:18.475 → request      session/list
13:14:18.540 ← response     result
full shell output
Codex> same output but not in a code block

▼ Notices

�[2m2026-05-22T17:14:13.691285Z�[0m �[31mERROR�[0m �[2mcodex_core::rollout::recorder�[0m�[2m:�[0m Falling back on rollout system

| Col 1 | Col 2 | Col 3 | Col 4 | Col 5 |
├────────┼───┼───┼───┼───┤
│ Row 1 │ A │ B │ C │ D │
│ Row 2 │ A │ B │ C │ D │
│ Row 3 │ A │ B │ C │ D │
│ Row 4 │ A │ B │ C │ D │
│ Row 5 │ A │ B │ C │ D │
│ Row 6 │ A │ B │ C │ D │
│ Row 7 │ A │ B │ C │ D │
│ Row 8 │ A │ B │ C │ D │
│ Row 9 │ A │ B │ C │ D │
│ Row 10 │ A │ B │ C │ D │

@xenodium
Copy link
Copy Markdown
Owner Author

thanks! almost there...

The buffer that has this:

13:14:13.690 → request      session/list
13:14:13.692 → request      session/prompt
13:14:13.754 ← response     result
13:14:15.454 ← notification session/update
13:14:15.455 ← notification session/update
13:14:15.482 ← notification session/update
13:14:15.483 ← notification session/update

Press C-x C-s (acp-traffic-save-to) to get the actual content of each one of those items

@mplanchard
Copy link
Copy Markdown

Oh lol, yeah, I thought that output didn't seem very useful. It's okay, one of these days I'll learn to read.

agent-shell-traffic.txt

weird that GH doesn't let you upload .el files, so I made it .txt

@xenodium
Copy link
Copy Markdown
Owner Author

Thanks that helps. Made some changes. Mind trying it out and see if you still have issues with headers on Codex?

xenodium added 14 commits May 23, 2026 15:23
- avoid-ranges is now a sorted vector; --in-avoid-range-p does binary search and returns the containing range.
- --replace-* passes use that return value to jump past avoid-ranges instead of re-matching inside them.
- --find-tables skips avoid-ranges in one hop and uses forward-line 1 between non-matches (table regex is bol-anchored).
Skip re-rendering already-processed prefix on each call

Streaming use of `agent-shell-markdown-replace-markup' calls the
renderer once per chunk, so every pass was re-walking the entire
buffer from `point-min' on each call — O(N^2) over N chunks.

Track a per-buffer "watermark": the position before which content
is fully rendered and stable.  Stored as an
`agent-shell-markdown-watermark' text property on the first
character (so a propertized string returned from
`agent-shell-markdown-convert' carries it without a buffer-local
variable).  Re-stamped at the end of each render to:
- start of the last line in the buffer; clamped back to
- start of any open fence (so a future closing ``` still matches),
- start of any rendered table whose extension is still possible
  (so streamed continuation rows still fold in).

The next call narrows to (watermark, point-max) and every pass
runs inside the narrow.

`:force' on `agent-shell-markdown-replace-markup' drops the
watermark and re-renders the whole buffer.
@mplanchard
Copy link
Copy Markdown

Thanks that helps. Made some changes. Mind trying it out and see if you still have issues with headers on Codex?

Yeah that seems to have done the trick!

image

@xenodium
Copy link
Copy Markdown
Owner Author

Awesome. Thanks for reporting back!

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants