Skip to content

Conversation

@remorses
Copy link
Contributor

@remorses remorses commented Dec 19, 2025

  • Adds MarkdownRenderable that extends CodeRenderable with markdown-specific features
  • Uses marked to parse markdown and automatically align table columns based on content width
  • Accounts for concealed characters (formatting markers like backticks, bold, italic) when calculating column widths
  • Related to feat: add automatic markdown table alignment #369 (table alignment) but this implements it as a dedicated Markdown renderable instead of modifying CodeRenderable
Screenshot 2025-12-20 at 00 44 44

Tables are rendered via flexbox boxes:

Here's the current structure:
tableBox (flexDirection: "row")
├── columnBox[0] (flexDirection: "column", border: top/bottom/left)
│   ├── headerBox (border: bottom)
│   │   └── TextRenderable "Emoji" (paddingLeft: 1, paddingRight: 1)
│   ├── cellBox (border: bottom)
│   │   └── TextRenderable "🎉"
│   ├── cellBox (border: bottom)
│   │   └── TextRenderable "🚀"
│   └── TextRenderable "日本語"
│
├── columnBox[1] (flexDirection: "column", border: all sides)
│   ├── headerBox
│   │   └── TextRenderable "Name"
│   ├── cellBox
│   │   └── TextRenderable "Party"
│   ...

@remorses remorses marked this pull request as ready for review December 20, 2025 00:10
Copilot AI review requested due to automatic review settings December 20, 2025 00:10
Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR adds a new MarkdownRenderable component that extends the rendering capabilities of OpenTUI with markdown-specific features. The implementation uses the marked library to parse markdown and renders tables with automatic column sizing through a BoxRenderable-based layout system.

Key changes:

  • Introduces MarkdownRenderable class with support for tables, code blocks, headings, lists, blockquotes, and inline formatting
  • Implements a conceal mode that hides markdown formatting markers (e.g., **, *, `) while preserving styled text
  • Adds comprehensive test coverage with 32 test cases covering various markdown features

Reviewed changes

Copilot reviewed 7 out of 8 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
packages/core/src/renderables/Markdown.ts New MarkdownRenderable implementation with table rendering, inline formatting, and concealment logic
packages/core/src/renderables/tests/Markdown.test.ts Comprehensive test suite covering tables, formatting, code blocks, and conceal modes
packages/core/src/renderables/index.ts Exports the new Markdown renderable
packages/core/src/examples/markdown-demo.ts Interactive demo showcasing markdown features with theme switching
packages/react/src/types/components.ts TypeScript types for React MarkdownRenderable component
packages/solid/src/types/elements.ts TypeScript types for Solid MarkdownRenderable component
packages/core/package.json Adds marked@^17.0.1 dependency
bun.lock Lock file updates for marked dependency

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@kommander
Copy link
Collaborator

Uhh looks good! I need to work through that.

@kommander
Copy link
Collaborator

It currently does not support streaming markdown properly. I can look into that. It needs an append API to append markdown chunks when they come in from an LLM response, only reparsing with marked what is not yet a finished block and not yet added as renderables, without removing and recreating all renderables for every update.

Show only complete rows (N-1) during streaming to avoid partial row flicker.
Rebuild tables only when complete row count changes, not on every cell update.
@remorses
Copy link
Contributor Author

remorses commented Dec 22, 2025

I updated table rendering to show a row at a time during streaming, this should improve performance (because right now tables rerender fully each time) and looks less janky

@kommander
Copy link
Collaborator

Cool, that works better. It does not support updating itself when conceal or syntaxStyle changes though it seems, which would cause issues for many opencode users I think. That would be the last detail to make it work for opencode, besides the table rendering not rendering full border joins, but it looks good as-is now and we can refine the table rendering in another iteration.

@remorses
Copy link
Contributor Author

with full table border joins you mean not overlapping border right? should be an easy fix with custom borders

@kommander
Copy link
Collaborator

That works. Switching the theme in the demo blocks the main thread for ~1s. Is that marked parsing? That's one reason I made tree-sitter threaded, to not block when it takes longer for any reason.

@remorses
Copy link
Contributor Author

Most time is spend on rendering, because I just clear all the renderables when switching conceal and theme

I can probably only update fields in every renderable right? This should be much faster

Screenshot 2025-12-23 at 15 34 32 Screenshot 2025-12-23 at 15 34 25

@remorses
Copy link
Contributor Author

Tried rerendering instead of recreating the renderable and got a bit worse. But I cannot reproduce the 1 second freeze. For me it freezes only 300ms or less

Instead of immediately rebuilding all blocks when syntaxStyle or conceal
changes, mark dirty and defer work to renderSelf(). This follows the same
pattern as CodeRenderable and avoids redundant work.

Key improvements measured via CPU profiling:
- textBufferSetStyledText: 113ms → 67ms (↓41%)
- updateLayout: 99ms → 17ms (↓83%)
- render: 160ms → 63ms (↓61%)
- Total profile time: 3091ms → 2886ms (↓7%)
@remorses
Copy link
Contributor Author

Added a performance fix for theme/conceal switching.

Problem: When syntaxStyle or conceal changed, we were clearing all block states and rebuilding everything from scratch (reparsing markdown, recreating all renderables).

Fix: Now we defer the work to renderSelf() and only update existing renderables instead of destroying/recreating them. This follows the same pattern as CodeRenderable which uses _highlightsDirty.

CPU profiling results (theme switch in the demo):

Function Before After Improvement
textBufferSetStyledText 113ms 67ms ↓41%
updateLayout 99ms 17ms ↓83%
render 160ms 63ms ↓61%
createTextBuffer 35ms 19ms ↓47%

Estimated theme switch latency: ~100-150ms (down from ~300ms)

@kommander
Copy link
Collaborator

I can only check that tomorrow, not allowed to touch the keyboard today. Happy holidays!

@remorses
Copy link
Contributor Author

Merry Christmas!

@kommander
Copy link
Collaborator

I made it update the tables in-place instead of recreating all elements, which is somewhat better. I found that when streaming a lot of markdown (endless mode in the demo), it becomes slower and slower. Seems like something is resetting the parserState so it re-parses the markdown and/or the tables are re-created for incremental updates which is expensive and in long content takes >10ms. There are some perf updates for text rendering coming in core, but I wouldn't rely on that.

I wonder if there is a better way to render tables, without relying on yoga layout, as for large tables it would be quite a lot of nested nodes.

@remorses
Copy link
Contributor Author

Thank you for fixing the reset. I think using Yoga is completely fine. I noticed no lags rendering a lot of tables in opencode with the markdown renderable.

@kommander
Copy link
Collaborator

Screen.Recording.2025-12-29.at.11.32.01.mov

Check the "Overall" render time in the stats, it should be low and stable, but it increases the more content is being streamed. In the markdown demo when I set stream speed to fastest and shift+E for endless stream (which can be stopped with shift+X again).

@remorses
Copy link
Contributor Author

remorses commented Dec 29, 2025

I found what was the issue with the lower FPS when the is content growing, the issue is layout recalculations

Every time a yoga node changes we also mark the parents dirty, so yoga calls measure functions again which can be slow when there are a lot of nodes

Using bun-yoga makes this problem less noticeable but does not remove it

To make layout calculation faster during streaming with a lot of content there would need to be a way to cache layout for nodes if content did not change like the browser does

Screenshot 2025-12-29 at 19 40 09

@remorses
Copy link
Contributor Author

Adding a cache for measureFunc in packages/core/src/renderables/TextBufferRenderable.ts lowers the time 3x, Yoga seems to call measure 3 times but we can cache it

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