Skip to content

perf(ui): lazy-load markdown and defer diff rendering#215

Merged
shantur merged 8 commits intoNeuralNomadsAI:devfrom
Pagecran:ready/ui-markdown-diff-perf
Mar 22, 2026
Merged

perf(ui): lazy-load markdown and defer diff rendering#215
shantur merged 8 commits intoNeuralNomadsAI:devfrom
Pagecran:ready/ui-markdown-diff-perf

Conversation

@pascalandr
Copy link
Copy Markdown
Contributor

@pascalandr pascalandr commented Mar 14, 2026

Summary

  • lazy-load the markdown and diff render paths so they stop inflating initial UI startup work
  • move shared text rendering helpers out of the markdown path and keep diff rendering on the deferred path
  • defer the Monaco secondary viewers so the markdown and diff path no longer keeps that work in the main bundle

Follow-ups

Testing

  • npm run typecheck --workspace @codenomad/ui
  • npm run build --workspace @codenomad/ui

@shantur
Copy link
Copy Markdown
Collaborator

shantur commented Mar 14, 2026

Hi @pascalandr

Thanks for the PR

Would you be able to explain the issue that this PR solves?

Thanks

@pascalandr
Copy link
Copy Markdown
Contributor Author

This PR reduces startup and initial-render work in the UI by moving markdown rendering and diff/highlighting code off the initial bundle path.

Before this change, the app paid the markdown + diff viewer cost eagerly even when the user had not opened content that needed those renderers yet. The result was a heavier initial UI load and slower perceived startup. This PR lazy-loads those paths and slims the diff highlighter dependency so we only pay that cost when the user actually opens markdown or diff-heavy content.

@shantur
Copy link
Copy Markdown
Collaborator

shantur commented Mar 15, 2026

@CodeNomadBot

Validate the issue if its present
Validate if the fix is correct
Check if fix brings in any regressions ?
Any other suggestions for the PR?
Any hidden features coming in as part of PR ?

Be the gatekeeper and check if this PR fixes what it says, and does that keeping the coding standards

@codenomadbot
Copy link
Copy Markdown
Contributor

codenomadbot Bot commented Mar 15, 2026

Looked through the PR with the “gatekeeper” lens: does it fix the stated startup/perf issue, any regressions, any hidden scope.

What I understand the issue to be

  • UI startup/initial render was paying the cost of markdown rendering + diff/highlighting code even before the user opened content requiring it.

Does this PR address that?

  • Yes, in code: packages/ui/src/App.tsx removes eager initMarkdown(...) so the markdown/shiki path is no longer initialized at app startup.
  • Markdown rendering is now deferred via dynamic import in packages/ui/src/components/markdown.tsx (import("../lib/markdown")). This should move the markdown + shiki bundle to a secondary chunk.
  • Tool-call diff rendering is deferred via Solid lazy() in packages/ui/src/components/tool-call/diff-render.tsx (import("../diff-viewer")).
  • Diff syntax highlighting dependency is slimmed by routing @git-diff-view/lowlight to a local lowlight(common) implementation: packages/ui/src/lib/git-diff-lowlight.ts + alias in packages/ui/vite.config.ts.
  • FilesTab/GitChangesTab also now lazy-load Monaco viewers (packages/ui/src/components/instance/shell/right-panel/tabs/FilesTab.tsx, packages/ui/src/components/instance/shell/right-panel/tabs/GitChangesTab.tsx).

Validation (local)

  • npm run typecheck --workspace @codenomad/ui: OK
  • npm run build --workspace @codenomad/ui: OK
  • Build warning worth noting: Monaco diff viewer will not end up in a separate chunk yet because packages/ui/src/components/instance/shell/right-panel/tabs/ChangesTab.tsx still statically imports it. Vite output:
    • monaco-diff-viewer.tsx is dynamically imported by GitChangesTab.tsx but also statically imported by ChangesTab.tsx, so it won’t move to another chunk.

Potential regressions / behavior changes

  • First-time markdown render can “flash” as plain escaped text (fallback) until the markdown chunk loads (packages/ui/src/components/markdown.tsx sets fallback HTML, then async-renders real markdown). Probably acceptable for perf, but it is a visible UX change.
  • Syntax highlighting for fenced code blocks becomes best-effort / eventually-consistent: initial render may be unhighlighted, then re-render after language loads (by design).
  • Markdown now hard-requires a text part id and throws if missing (packages/ui/src/components/markdown.tsx: "Markdown rendering requires a part id"). If there is any path where text parts can be missing ids (older persisted data, server edge cases), this could become a hard crash.

Hidden features / scope creep

  • I don’t see new product features beyond the perf/lazy-load changes + the (expected) UX tradeoffs above. No new APIs or permissions changes.

Suggestions (tight scope)

  1. If the goal includes Monaco diff chunking: update packages/ui/src/components/instance/shell/right-panel/tabs/ChangesTab.tsx to lazy-load Monaco too, otherwise the dynamic import in GitChangesTab.tsx won’t actually reduce the initial chunk.
  2. Consider softening the part.id requirement in packages/ui/src/components/markdown.tsx (fallback cache key derived from text/hash instead of throwing) unless you’re 100% sure ids are always present.
  3. Optional: preload the markdown chunk after initial idle (e.g. requestIdleCallback) to reduce the “raw markdown” flash while still keeping it off the critical path.

--
Yours,
CodeNomadBot

@shantur
Copy link
Copy Markdown
Collaborator

shantur commented Mar 15, 2026

@CodeNomadBot

What is packages/ui/src/lib/git-diff-lowlight.ts doing the PR ?

@codenomadbot
Copy link
Copy Markdown
Contributor

codenomadbot Bot commented Mar 15, 2026

packages/ui/src/lib/git-diff-lowlight.ts is a local replacement for the @git-diff-view/lowlight dependency used by the diff viewer.

What it does in this PR

  • Implements the “lowlight highlighter” adapter (the highlighter export with getAST, processAST, etc.) using lowlight(common) instead of pulling in the full/all-languages bundle.
  • Adds a small custom registration for vue (maps to xml + embedded script/style blocks) so Vue diffs still highlight reasonably.
  • Keeps the public shape that @git-diff-view/* expects, so the diff viewer keeps working without code changes upstream.

How it’s wired

  • packages/ui/vite.config.ts sets an alias:
    • "@git-diff-view/lowlight" -> "./src/lib/git-diff-lowlight.ts"
      So any import of @git-diff-view/lowlight in the UI bundle resolves to this local file.

Why it exists

  • Bundle-size/perf: common is much smaller than the all-language variant, so this helps the PR goal of slimming the diff/highlighting path and keeping it off the initial startup bundle.

Tradeoff

  • Highlighting coverage is limited to “common” languages (plus the custom vue registration). For uncommon languages it will fall back to highlightAuto (best-effort) rather than guaranteed dedicated grammar support.

--
Yours,
CodeNomadBot

@shantur
Copy link
Copy Markdown
Collaborator

shantur commented Mar 16, 2026

@pascalandr - Can you please review the "Potential regressions / behavior changes" mentioned

@pascalandr
Copy link
Copy Markdown
Contributor Author

That was a fair callout.

Markdown rendering no longer throws when a text part has no id. Instead, it now falls back to a content-based cache key so older persisted data / edge-case parts do not hard-crash the render path.

I also kept the Monaco/right-panel chunk-splitting work out of this base PR. That part now lives in the stacked follow-up on my fork (ready/ui-secondary-chunks / Pagecran#1), so this PR stays scoped to:

  • lazy-loading the markdown and diff render paths,
  • moving shared text rendering helpers out of the markdown path,
  • replacing the heavier diff highlighting dependency with a slimmer deferred path.

The remaining behavior change is the expected deferred-render tradeoff: markdown may initially render with fallback HTML and fenced code highlighting can become progressively enhanced once the renderer/language data finishes loading, but the hard crash case is now addressed.

@shantur
Copy link
Copy Markdown
Collaborator

shantur commented Mar 16, 2026

@CodeNomadBot

Check the current PR state

@codenomadbot
Copy link
Copy Markdown
Contributor

codenomadbot Bot commented Mar 16, 2026

Current PR state (local checkout)

What I’m checking

  • Latest head commit(s), diff vs dev, and whether the previously-called-out markdown part.id hard-crash is now addressed.

State

  • HEAD: 3d40f70 fix(ui): tolerate markdown parts without ids
  • Commits unique to this branch vs dev: 28 total
    • 4 non-merge (the actual PR commits): f558616, 2362b11, 9bea4e2, 3d40f70
    • 24 merge commits (history noise, no net file diff vs dev)
  • Diff vs dev: 14 files changed, +557/-252 (still scoped to UI perf/lazy-load work)
  • Working tree: clean

Regression status

  • The prior potential crash is fixed: packages/ui/src/components/markdown.tsx no longer throws when part.id is missing; it falls back to a content-hash cache id (anonymous:<hash>).
  • Remaining intended behavior change still applies: first markdown render may show fallback HTML until the lazy-loaded markdown/shiki path finishes loading; fenced highlighting is progressively enhanced.

Quick verification

  • npm run typecheck --workspace @codenomad/ui: OK on current HEAD

Note

  • Monaco chunk splitting is still limited by the static import in packages/ui/src/components/instance/shell/right-panel/tabs/ChangesTab.tsx (so GitChangesTab.tsx’s lazy import won’t fully split that module). Author noted this is intentionally deferred to the stacked follow-up PR.

--
Yours,
CodeNomadBot

@pascalandr pascalandr force-pushed the ready/ui-markdown-diff-perf branch from 16ee70f to f1696aa Compare March 18, 2026 19:57
@pascalandr pascalandr changed the base branch from main to dev March 18, 2026 19:57
@pascalandr
Copy link
Copy Markdown
Contributor Author

Updated this to target dev properly.

I rebuilt the branch on top of dev, removed the unrelated auth, Electron, and server changes from the diff, and kept only the markdown and diff loading work in this PR. I also reran the UI build on the rebuilt branch.

@pascalandr
Copy link
Copy Markdown
Contributor Author

Updated this again after the dev retarget work.

I rebuilt the branch cleanly on top of dev, force-pushed it, and kept the diff limited to the markdown and diff rendering work only. I also restacked the follow-up branch on top of this refreshed base.

@shantur
Copy link
Copy Markdown
Collaborator

shantur commented Mar 18, 2026

@CodeNomadBot

  • Perform PR checks again

@codenomadbot
Copy link
Copy Markdown
Contributor

codenomadbot Bot commented Mar 18, 2026

Re-ran PR checks on current branch head.

What I understood

  • You want the CI-equivalent sanity checks re-run after the recent force-push / dev retarget updates.

Results (local)

  • HEAD: f1696aa fix(ui): retry deferred markdown renderer setup
  • UI typecheck: npm run typecheck --workspace @codenomad/ui
  • UI build: npm run build --workspace @codenomad/ui

Notable build warnings (unchanged)

  • Monaco diff chunking: monaco-diff-viewer.tsx is still statically imported by packages/ui/src/components/instance/shell/right-panel/tabs/ChangesTab.tsx, so the dynamic import in GitChangesTab.tsx will not move it to a separate chunk.
  • Some chunks exceed 500kB after minification (Vite advisory warning).

Branch position vs base

  • Relative to origin/dev, this branch is ahead 5 / behind 10 commits (git rev-list --left-right --count origin/dev...HEAD).

--
Yours,
CodeNomadBot

@shantur
Copy link
Copy Markdown
Collaborator

shantur commented Mar 18, 2026

@CodeNomadBot

I want you to perform the gatekeeper checks for latest PR state

  • check for regressions
  • check for better possible implementations
  • Be the PR gatekeeper

@codenomadbot
Copy link
Copy Markdown
Contributor

codenomadbot Bot commented Mar 18, 2026

Gatekeeper pass on latest PR state (current HEAD) focusing on regressions + implementation quality.

What I’m checking / assumptions

  • You want a fresh “does this PR still do what it says” review after the force-push / dev retarget.
  • I’m validating scope, build/type safety, and looking for likely runtime/UX regressions.

Current state

  • HEAD: f1696aa fix(ui): retry deferred markdown renderer setup
  • Diff scope vs origin/dev: only packages/ui/** (14 files), centered on markdown + diff rendering + helpers.
  • Local checks: npm run typecheck --workspace @codenomad/ui ✅, npm run build --workspace @codenomad/ui

Does it still fix the stated issue?

  • Yes: markdown/shiki work is removed from app startup (packages/ui/src/App.tsx no longer calls initMarkdown), and markdown rendering is deferred via dynamic import in packages/ui/src/components/markdown.tsx.
  • Diff viewer for tool-call diffs is deferred via Solid lazy() + Suspense (packages/ui/src/components/tool-call/diff-render.tsx).
  • Diff highlighting dependency is slimmed by aliasing @git-diff-view/lowlight to a local lowlight(common) adapter (packages/ui/src/lib/git-diff-lowlight.ts + packages/ui/vite.config.ts).

Regression watchlist (what could break)

  • UX: first-time markdown render still intentionally “progressively enhances” (fallback escaped HTML first, then re-render when markdown chunk/highlighter/languages are ready). This is expected but noticeable.
  • Highlight coverage: lowlight(common) reduces supported grammars; uncommon languages will rely on highlightAuto (acceptable tradeoff, but a behavior change).
  • Monaco chunking: build warning remains — packages/ui/src/components/instance/shell/right-panel/tabs/ChangesTab.tsx statically imports Monaco diff viewer, so the dynamic import in GitChangesTab.tsx won’t actually split that module into a secondary chunk.

Implementation quality notes (what looks good)

  • The “hard-crash when part.id is missing” callout is addressed: markdown cache id falls back to anonymous:<hash> (no throw).
  • New retry behavior is solid: both the deferred markdown module loader and the shiki highlighter init reset their memoized promises on failure, so a transient import/init failure doesn’t permanently brick rendering.
  • Shared helpers moved out of markdown (packages/ui/src/lib/text-render-utils.ts) reduces coupling and keeps the markdown path slimmer.

Suggestions (tight scope; not required for merge)

  1. i18n/accessibility: packages/ui/src/components/instance/shell/right-panel/tabs/GitChangesTab.tsx still has overlayAriaLabel="Git Changes" hardcoded. This is user-visible to screen readers; prefer props.t("instanceShell.rightPanel.tabs.gitChanges").
  2. decodeHtmlEntitiesLocally in packages/ui/src/components/markdown.tsx uses a DOM textarea decode. If the goal is consistency + testability, you could consider using decodeHtmlEntities from packages/ui/src/lib/text-render-utils.ts instead (but I understand avoiding extra imports in hot paths).

No hidden features spotted

  • No auth/server/electron changes in the current diff; PR is scoped to markdown/diff perf + lazy-load behavior.

--
Yours,
CodeNomadBot

@pascalandr
Copy link
Copy Markdown
Contributor Author

I took the latest gatekeeper pass into account.

I applied the actionable follow-up from that review in this branch: the Git Changes overlay aria label now goes through the existing translation key instead of staying hardcoded.

The other notes in the latest bot pass looked non-blocking to me:

  • the Monaco chunk splitting limitation is still intentionally deferred to the stacked follow-up
  • the decodeHtmlEntities helper suggestion would broaden the scope of this base PR more than I wanted

So I kept this PR focused on the markdown and diff loading changes themselves.

@pascalandr
Copy link
Copy Markdown
Contributor Author

I reshuffled the split a bit.

I moved the Monaco-specific secondary viewer chunking into this base branch, since it directly completes the lazy markdown and diff story here. The remaining right panel, picker, and tool-call chunking work stays in Pagecran#1, which now targets dev directly instead of stacking on top of this branch.

@shantur
Copy link
Copy Markdown
Collaborator

shantur commented Mar 21, 2026

@CodeNomadBot

I want you to perform the gatekeeper checks for latest PR state

check for regressions
check for better possible implementations
Be the PR gatekeeper

@codenomadbot
Copy link
Copy Markdown
Contributor

codenomadbot Bot commented Mar 21, 2026

Gatekeeper checks (latest PR state as of current HEAD)

What I understood

  • You want a fresh “gatekeeper” pass on the current PR tip: regressions risk + implementation quality + any better approaches.

Current PR state (local)

  • HEAD: b6d5b80 perf(ui): defer Monaco secondary viewers
  • Working tree: clean
  • Branch vs origin/dev: behind 28 / ahead 7 commits (git rev-list --left-right --count origin/dev...HEAD)

Verification

  • npm run typecheck --workspace @codenomad/ui: ✅
  • npm run build --workspace @codenomad/ui: ✅

Does it still match PR intent?

  • Yes: markdown + diff render paths remain deferred (dynamic import() / Solid lazy()), and now Monaco secondary viewers are also deferred in the right-panel tabs.
  • Prior “Monaco diff viewer not actually chunk-splitting due to static import” issue is fixed: packages/ui/src/components/instance/shell/right-panel/tabs/ChangesTab.tsx is now lazy-loading Monaco too, and the earlier Vite warning is gone.

Regression watchlist

  • UX tradeoff remains (by design): markdown may render fallback HTML first, then re-render after the markdown/highlighter path loads.
  • Syntax highlighting coverage is reduced for diffs (slimmer lowlight(common)): uncommon languages may rely on auto-detect.
  • New chunking strategy: packages/ui/vite.config.ts adds manualChunks(...) to group vendor code (git-diff/highlight/monaco/etc). This should help caching/initial-load, but it does change chunk boundaries (worth being aware for PWA cache behavior / deploy diffs).

Coding standards / hygiene

  • i18n: the previously hardcoded Git Changes overlay label + other strings are now translated, and the new instanceShell.gitChanges.* keys are present across locales.
  • Scope: changes are still confined to packages/ui/**.

Suggestions (non-blocking)

  1. Consider updating/rebasing on latest dev before merge (branch is currently behind origin/dev by 28 commits), to reduce merge risk.
  2. Optional: keep an eye on the highlight-vendor chunk size (still large, expected with syntax highlighting) to ensure it stays off the critical path.

--
Yours,
CodeNomadBot

@shantur
Copy link
Copy Markdown
Collaborator

shantur commented Mar 21, 2026

@CodeNomadBot

Give me detailed analysis for

  • Syntax highlighting coverage is reduced for diffs (slimmer lowlight(common)): uncommon languages may rely on auto-detect.
  • New chunking strategy: packages/ui/vite.config.ts adds manualChunks(...) to group vendor code (git-diff/highlight/monaco/etc). This should help caching/initial-load, but it does change chunk boundaries (worth being aware for PWA cache behavior / deploy diffs).

@codenomadbot
Copy link
Copy Markdown
Contributor

codenomadbot Bot commented Mar 21, 2026

Detailed analysis on the two gatekeeper callouts.

  1. Diff syntax highlighting coverage (lowlight(common))

What changed

  • The PR replaces the upstream @git-diff-view/lowlight module with a local adapter in packages/ui/src/lib/git-diff-lowlight.ts and wires it via Vite alias.
  • That adapter instantiates lowlight with createLowlight(common) (a “common languages only” highlight.js grammar set).

How highlighting is selected at runtime (important)

  • highlighter.getAST(raw, fileName?, lang?) does:
    • If lang is provided AND lowlight.registered(lang) is true -> lowlight.highlight(lang, raw) (deterministic).
    • Else -> lowlight.highlightAuto(raw) (heuristic language detection).
  • In other words: “coverage reduction” only impacts cases where the caller asks for a language that is NOT registered in the common set.

What “reduced coverage” means in practice

  • Previously: you effectively had “all”/very broad language coverage (or at least a much larger set) so lang from file extension often matched a registered grammar.
  • Now: only languages included in common (plus PR-added vue) are guaranteed to be highlighted when explicitly requested.
  • When the language is missing:
    • Highlighting might still happen via highlightAuto, but:
      • It can mis-classify (e.g., treat Kotlin as Java, or treat TS as JS if TS grammar isn’t registered).
      • It can choose “plaintext” (no highlight) for uncommon syntaxes.
      • It can be slower than direct highlighting (auto-detect runs multiple grammars).

Where this shows up for CodeNomad

  • The diff viewer passes a fileLang derived from path extension (see packages/ui/src/lib/text-render-utils.ts + packages/ui/src/components/diff-viewer.tsx).
  • If your repo contains uncommon languages (examples: nix, coq, raku, lean, zig, etc.), those may no longer match a registered grammar and will fall back to highlightAuto.
  • The PR adds a targeted vue registration to avoid a common “missing grammar” case for .vue SFCs.

Mitigations / knobs already in the adapter

  • ignoreSyntaxHighlightList: can disable highlighting for specific file paths/patterns (useful for huge/minified/vendor files).
  • maxLineToIgnoreSyntax: allows the upstream diff lib to skip highlighting beyond a line threshold (perf protection; how it’s applied depends on @git-diff-view/* internals).

What I’d watch for (regression signals)

  • A diff that used to have stable token colors now looks “random” or inconsistent between renders (auto-detect variance).
  • CPU spikes when opening diffs of “unknown” languages (auto-detect cost).

If you want a “better” implementation (tradeoffs)

  • Current approach is the right “bundle-first” choice.
  • If accuracy is more important for certain languages, the next step would be to explicitly register a few extra grammars beyond common (targeted add-ons), instead of switching back to an all-languages bundle.
  1. manualChunks(...) in packages/ui/vite.config.ts

What it does

  • This PR adds a Rollup chunking policy that groups modules by path into named bundles:
    • git-diff-vendor: everything under /node_modules/@git-diff-view/
    • highlight-vendor: /node_modules/highlight.js/ and /node_modules/lowlight/
    • fast-diff-vendor: /node_modules/fast-diff/
    • monaco-vendor: /node_modules/monaco-editor/
    • monaco-viewer: CodeNomad’s Monaco viewer implementation (/src/components/file-viewer/ and /src/lib/monaco/)

Why this usually helps

  • Better caching: a change in app code won’t invalidate big third-party chunks as often.
  • Better startup path control: large feature/vendor code can be kept out of the main entry chunk and only pulled when needed.

Concrete output from the current build (so we’re not hand-wavy)

  • The build now emits these notable chunks:
    • assets/highlight-vendor-*.js ~926 kB (gzip ~301 kB)
    • assets/git-diff-vendor-*.js ~167 kB (gzip ~44 kB)
    • assets/monaco-viewer-*.js ~90 kB (gzip ~30 kB)
    • assets/fast-diff-vendor-*.js ~7.8 kB (gzip ~3.1 kB)
    • assets/main-*.js ~911 kB (gzip ~266 kB)

What “changing chunk boundaries” impacts (deploy/PWA)

  • Deploy diffs: you’ll see different files in dist/assets/ (more, smaller named chunks). That’s expected.
  • PWA precache list changes: Workbox precache entries increase/shift because filenames change.
    • In packages/ui/src/renderer/dist/sw.js, Workbox calls precacheAndRoute([...]) with a large asset list.
    • For hashed assets, many entries use revision: null (Workbox treats the URL as versioned because the filename has a hash).
  • Practical impact:
    • Existing users will download a different set of JS files on update (but only once; then they’re cached).
    • If you’re sensitive to “first-load bandwidth”, note that Workbox precache (as currently configured with globPatterns including **/*.js) will still cache all emitted JS chunks during SW install, even if the app lazy-loads them at runtime.
      • Lazy-loading still reduces initial JS parse/execute, but it may not reduce bytes fetched once the SW kicks in.

Regression watchlist

  • Over-splitting can increase request count (usually fine with HTTP/2 / local desktop, but worth noting).
  • Any mis-grouping in manualChunks can cause “unexpected” chunk dependencies (rare, but if a chunk becomes too big again, check the paths).

If you want a “better” implementation (optional)

  • If the goal is also to reduce PWA install-time downloads, consider excluding the largest deferred-only chunks from precache (Workbox globIgnores) and let runtimeCaching fetch/cache them on demand. That’s a product decision (offline-first vs bandwidth-first), not a correctness issue.

--
Yours,
CodeNomadBot

@shantur
Copy link
Copy Markdown
Collaborator

shantur commented Mar 21, 2026

@CodeNomadBot

Can you list the languages supported pre and post PR changes

1 similar comment
@shantur
Copy link
Copy Markdown
Collaborator

shantur commented Mar 22, 2026

@CodeNomadBot

Can you list the languages supported pre and post PR changes

@codenomadbot
Copy link
Copy Markdown
Contributor

codenomadbot Bot commented Mar 22, 2026

Language support for diff syntax highlighting (the @git-diff-view/solid renderer) before vs after this PR.

  • Pre-PR: upstream @git-diff-view/lowlight@0.0.35 bundles 192 highlight.js grammars
  • Post-PR: local packages/ui/src/lib/git-diff-lowlight.ts registers 38 grammars (37 from lowlight(common) + custom vue)

Impact on our file-extension -> language inference (packages/ui/src/lib/text-render-utils.ts):

  • Missing from post-PR registered set (will fall back to auto-detect): html
Post-PR registered languages (38)
arduino
bash
c
cpp
csharp
css
diff
go
graphql
ini
java
javascript
json
kotlin
less
lua
makefile
markdown
objectivec
perl
php
php-template
plaintext
python
python-repl
r
ruby
rust
scss
shell
sql
swift
typescript
vbnet
vue
wasm
xml
yaml
Pre-PR bundled highlight.js languages (192)
1c
abnf
accesslog
actionscript
ada
angelscript
apache
applescript
arcade
arduino
armasm
asciidoc
aspectj
autohotkey
autoit
avrasm
awk
axapta
bash
basic
bnf
brainfuck
c
cal
capnproto
ceylon
clean
clojure
clojure-repl
cmake
coffeescript
coq
cos
cpp
crmsh
crystal
csharp
csp
css
d
dart
delphi
diff
django
dns
dockerfile
dos
dsconfig
dts
dust
ebnf
elixir
elm
erb
erlang
erlang-repl
excel
fix
flix
fortran
fsharp
gams
gauss
gcode
gherkin
glsl
gml
go
golo
gradle
graphql
groovy
haml
handlebars
haskell
haxe
hsp
http
hy
inform7
ini
irpf90
isbl
java
javascript
jboss-cli
json
julia
julia-repl
kotlin
lasso
latex
ldif
leaf
less
lisp
livecodeserver
livescript
llvm
lsl
lua
makefile
markdown
mathematica
matlab
maxima
mel
mercury
mipsasm
mizar
mojolicious
monkey
moonscript
n1ql
nestedtext
nginx
nim
nix
node-repl
nsis
objectivec
ocaml
openscad
oxygene
parser3
perl
pf
pgsql
php
php-template
plaintext
pony
powershell
processing
profile
prolog
properties
protobuf
puppet
purebasic
python
python-repl
q
qml
r
reasonml
rib
roboconf
routeros
rsl
ruby
ruleslanguage
rust
sas
scala
scheme
scilab
scss
shell
smali
smalltalk
sml
sqf
sql
stan
stata
step21
stylus
subunit
swift
taggerscript
tap
tcl
thrift
tp
twig
typescript
vala
vbnet
vbscript
vbscript-html
verilog
vhdl
vim
wasm
wren
x86asm
xl
xml
xquery
yaml
zephir

--
Yours,
CodeNomadBot

@shantur
Copy link
Copy Markdown
Collaborator

shantur commented Mar 22, 2026

@pascalandr - Is reduction in languages on purpose?

@pascalandr
Copy link
Copy Markdown
Contributor Author

Yes, it was intentional, as a secondary bundle-size/load-time optimization,
But it s optionnal, the point of this PR is still lazy-loading markdown/diff/Monaco work off the initial path. Do you want me to drop the reduced language coverage part?

@shantur
Copy link
Copy Markdown
Collaborator

shantur commented Mar 22, 2026

Yes please let's not reduce the language support, I am ready to merge this as soon as its done

@pascalandr pascalandr changed the title perf(ui): lazy-load markdown and slim diff rendering perf(ui): lazy-load markdown and defer diff rendering Mar 22, 2026
@pascalandr
Copy link
Copy Markdown
Contributor Author

Removed the local replacement, so diff highlighting now keeps the upstream language coverage. The PR is back to the lazy-loading / deferred rendering changes only, and I reran UI typecheck + build on this updated branch.

@pascalandr
Copy link
Copy Markdown
Contributor Author

Removed the local lowlight replacement, so diff highlighting keeps the upstream language coverage now. The branch only keeps the lazy-loading / deferred rendering changes, and I reran UI typecheck + build on it.

@shantur
Copy link
Copy Markdown
Collaborator

shantur commented Mar 22, 2026

@CodeNomadBot

I want you to perform the gatekeeper checks for latest PR state

check for regressions
check for better possible implementations
Be the PR gatekeeper

@codenomadbot
Copy link
Copy Markdown
Contributor

codenomadbot Bot commented Mar 22, 2026

Gatekeeper checks on latest PR state (after upstream diff-highlighting coverage was restored).

What I understood

  • You want a fresh merge-readiness pass: regressions risk + implementation quality + any better options.

Current state

  • HEAD: 9da932c revert(ui): keep upstream diff highlighting coverage
  • Local lowlight replacement is gone: no @git-diff-view/lowlight alias and no git-diff-lowlight.ts usage in the current diff.
  • Diff scope vs origin/dev: packages/ui/** only (20 files changed)

Verification

  • npm run typecheck --workspace @codenomad/ui: ✅
  • npm run build --workspace @codenomad/ui: ✅

Does it still do what the PR claims?

  • Yes.
    • Markdown rendering is lazy-loaded (packages/ui/src/components/markdown.tsx dynamically imports packages/ui/src/lib/markdown.ts).
    • Tool-call diff viewer is deferred (packages/ui/src/components/tool-call/diff-render.tsx uses lazy() + Suspense).
    • Monaco secondary viewers are deferred in right-panel tabs (packages/ui/src/components/instance/shell/right-panel/tabs/FilesTab.tsx, packages/ui/src/components/instance/shell/right-panel/tabs/ChangesTab.tsx, packages/ui/src/components/instance/shell/right-panel/tabs/GitChangesTab.tsx).

Regression watchlist

  • UX tradeoff remains (expected): markdown may initially show fallback HTML before the markdown chunk loads, then re-render.
  • manualChunks is still present in packages/ui/vite.config.ts. This changes output chunk boundaries (good for caching), but note it can also affect PWA install/update payloads.
  • Large bundles still exist (e.g. highlight-vendor ~926 kB before gzip in my build output). It’s now chunked separately, which is the key mitigation.

Implementation quality / standards

  • i18n: Git Changes strings and overlay aria labels now use translation keys (packages/ui/src/lib/i18n/messages/*/instance.ts updated; ChangesTab/FilesTab/GitChangesTab use props.t(...)).
  • Diff viewer: now uses getLanguageFromPath from the shared helper module (packages/ui/src/lib/text-render-utils.ts), keeping markdown and diff concerns separated.

Non-blocking suggestions

  1. Consider excluding some very large deferred chunks from Workbox precache if the goal includes reducing SW install-time downloads (product decision).
  2. Optional: in packages/ui/src/components/markdown.tsx, decodeHtmlEntitiesLocally could be replaced with the pure helper decodeHtmlEntities for consistency/testability (but current approach avoids extra imports and works).

--
Yours,
CodeNomadBot

@shantur shantur merged commit 74f753a into NeuralNomadsAI:dev Mar 22, 2026
11 of 12 checks passed
shantur added a commit that referenced this pull request Mar 27, 2026
…255)

## Thanks for contributions

- PR [#249](#249)
"feat(speech): add prompt voice input" by
[@shantur](https://github.com/shantur)
- PR [#243](#243)
"feat(i18n): Hebrew locale + full RTL support" by
[@MusiCode1](https://github.com/MusiCode1)
- PR [#241](#241)
"feat(lazy loading): Implement virtual list with virtua" by
[@pixellos](https://github.com/pixellos)
- PR [#240](#240)
"fix(tauri): force Windows process tree shutdown" by
[@pascalandr](https://github.com/pascalandr)
- PR [#239](#239)
"perf(ui): split right panel and secondary viewer chunks" by
[@pascalandr](https://github.com/pascalandr)
- PR [#238](#238)
"perf(ui): defer locale and overlay bundles" by
[@pascalandr](https://github.com/pascalandr)
- PR [#236](#236)
"Suppress OS notifications for subagent (child) sessions" by
`@app/codenomadbot`
- PR [#235](#235)
"fix(ui): unwrap pasted placeholders in slash commands" by
`@app/codenomadbot`
- PR [#232](#232)
"fix(tauri): stop CLI process group on exit" by `@app/codenomadbot`
- PR [#229](#229)
"feat(ui): add RTL support for Hebrew/Arabic text" by
[@MusiCode1](https://github.com/MusiCode1)
- PR [#227](#227)
"fix(tauri): improve Windows desktop runtime behavior" by
[@pascalandr](https://github.com/pascalandr)
- PR [#226](#226)
"fix(tauri): restore desktop menu controls and fullscreen shortcut" by
[@pascalandr](https://github.com/pascalandr)
- PR [#225](#225)
"fix(tauri): restore external links in the folder picker" by
[@pascalandr](https://github.com/pascalandr)
- PR [#224](#224)
"fix(tauri): sync server UI bundle during prebuild" by
[@pascalandr](https://github.com/pascalandr)
- PR [#215](#215)
"perf(ui): lazy-load markdown and defer diff rendering" by
[@pascalandr](https://github.com/pascalandr)

## Highlights

- **Voice-first conversations**: Start prompts with voice input,
configure speech behavior from settings, and listen back to assistant
responses with message playback and conversation playback controls.
- **A complete Hebrew + RTL experience**: CodeNomad now ships with a
full Hebrew locale and much broader right-to-left support, making the
app feel natural for Hebrew users while improving Arabic text rendering
too.
- **A much faster experience in long chats**: The new virtualized
message list, deferred markdown and diff rendering, and more selective
loading for heavy UI surfaces make large sessions feel noticeably
smoother.

## What's Improved

- **More flexible speech controls**: Speech settings and playback modes
now adapt better to different browsers and platform capabilities.
- **Cleaner prompt workflow**: The prompt includes a quick clear action,
a simpler recording indicator, and a more polished mic control layout.
- **Faster startup and lighter heavy views**: Locale bundles, overlays,
right-panel viewers, picker flows, markdown, and diff surfaces all load
more lazily to reduce upfront UI work.
- **Less notification spam**: Subagent sessions no longer fire OS
notifications, so important interruptions are easier to notice.
- **Better RTL behavior across the whole interface**: Session names,
tool outputs, markdown blocks, file views, selectors, and layout
controls behave more consistently in right-to-left contexts.

## Fixes

- **More reliable Windows desktop behavior**: Process cleanup is
stronger during app shutdown, background CLI process trees are
terminated more reliably, desktop identity/metadata is aligned more
cleanly, and stray console windows are hidden during startup and exit.
- **Cleaner shutdown on macOS and Linux**: Desktop quit/close now stops
the spawned CLI process group more reliably, reducing leftover
background processes after exit.
- **Restored desktop actions**: External links in the folder picker work
again, and the desktop View/Window controls plus the fullscreen shortcut
are back.
- **More stable streaming and scrolling**: Reasoning streams stay pinned
more consistently, follow behavior is less jumpy, spacing is cleaner in
virtualized conversations, and session switching retains position more
smoothly.
- **Safer slash command pasting**: Pasted placeholders are resolved
correctly before slash commands run, so long pasted inputs behave like
normal prompts.
- **More dependable desktop packaging**: Tauri prebuild now refreshes
the server UI bundle correctly, which avoids packaged desktop builds
picking up stale UI assets.
- **Clearer speech compatibility handling**: Streaming playback
limitations are surfaced more cleanly instead of failing in a confusing
way.

### Contributors

- [@pascalandr](https://github.com/pascalandr)
- [@MusiCode1](https://github.com/MusiCode1)
- [@pixellos](https://github.com/pixellos)
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