From 09284ee2ce9d64a1099cc97a9758ce789b34ca39 Mon Sep 17 00:00:00 2001
From: MusiCode1 <54508123+MusiCode1@users.noreply.github.com>
Date: Sun, 22 Mar 2026 22:18:24 +0200
Subject: [PATCH] feat(ui): add RTL support for Hebrew/Arabic text (#229)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
## What and why
CodeNomad had no RTL (right-to-left) support, so users writing in Hebrew
or Arabic would see their messages displayed left-to-right — misaligned
text, broken reading flow, wrong punctuation placement.
This PR adds automatic direction detection to all elements that display
user or model text. The browser detects direction from the first strong
character in each text block: Hebrew/Arabic → RTL, Latin/code → LTR. No
configuration needed — it just works per message, per paragraph.
## Technical notes
The natural fix is `dir="auto"` on the containing elements. However,
Chromium does not propagate direction detection from a parent `
`
into its `
` children — so Hebrew inside `
` rendered via
`innerHTML` (as markdown is) was still detected as LTR. The fix is to
apply `unicode-bidi: plaintext` via CSS directly on the block-level
elements (`p`, `li`, headings, etc.), which has the same auto-detection
semantics but applies per element.
## Summary
- Add `dir="auto"` to all elements containing user-generated or
model-generated text (message content, prompt input, session names, tool
outputs) so the browser auto-detects text direction
- Add `unicode-bidi: plaintext` via CSS to markdown block elements (`p`,
`li`, headings, `blockquote`, `td`/`th`) to fix per-paragraph RTL
detection in Chromium (where `dir="auto"` on a parent div does not
recurse into block children)
- Convert physical CSS properties to logical equivalents in
`markdown.css`: `border-left` → `border-inline-start`, `padding-left` →
`padding-inline-start`, `text-align: left` → `text-align: start`,
`margin-left` → `margin-inline-start`
## Affected components
- `markdown.tsx` — main markdown renderer
- `message-part.tsx` — text part wrapper and plain-text fallback
- `message-item.tsx` — message body and error blocks
- `prompt-input.tsx` — user input textarea
- `session-list.tsx` — session titles in sidebar
- `session-rename-dialog.tsx` — session rename input
- `instance-welcome-view.tsx` — Resume Session dialog
- `tool-call/markdown-render.tsx` — tool output markdown fallback
- `tool-call/ansi-render.tsx` — ANSI output
- `tool-call/diagnostics-section.tsx` — diagnostic messages
## Test plan
- [ ] Send a Hebrew-only message → text right-aligned
- [ ] Send a mixed Hebrew + English message → correct per-paragraph
direction
- [ ] Message containing a code block → code stays LTR
- [ ] Type Hebrew in the prompt textarea → input flows right-to-left
- [ ] Hebrew session name in sidebar → right-aligned
- [ ] Hebrew session name in Resume Session dialog → right-aligned
🤖 Generated with [Claude Code](https://claude.com/claude-code)
---------
Co-authored-by: Claude Sonnet 4.6
---
manifest.json | 6 ++++
.../src/components/instance-welcome-view.tsx | 1 +
packages/ui/src/components/markdown.tsx | 1 +
packages/ui/src/components/message-item.tsx | 4 +--
packages/ui/src/components/message-part.tsx | 3 +-
packages/ui/src/components/prompt-input.tsx | 1 +
packages/ui/src/components/session-list.tsx | 2 +-
.../src/components/session-rename-dialog.tsx | 1 +
.../src/components/tool-call/ansi-render.tsx | 2 +-
.../tool-call/diagnostics-section.tsx | 2 +-
.../components/tool-call/markdown-render.tsx | 2 +-
packages/ui/src/styles/markdown.css | 28 +++++++++++++++----
12 files changed, 41 insertions(+), 12 deletions(-)
create mode 100644 manifest.json
diff --git a/manifest.json b/manifest.json
new file mode 100644
index 00000000..d10e5612
--- /dev/null
+++ b/manifest.json
@@ -0,0 +1,6 @@
+{
+ "minServerVersion": "0.12.3",
+ "latestUIVersion": "0.12.3-rtl",
+ "uiPackageURL": "https://github.com/MusiCode1/CodeNomad/releases/download/v0.12.3-rtl/codenomad-ui-rtl.zip",
+ "sha256": "a2ce1aaa04345a2f9ca9d3c3149567867f3a5e477cf6eb269381e6dc1bec7ca2"
+}
diff --git a/packages/ui/src/components/instance-welcome-view.tsx b/packages/ui/src/components/instance-welcome-view.tsx
index 0d520a86..4233e0ae 100644
--- a/packages/ui/src/components/instance-welcome-view.tsx
+++ b/packages/ui/src/components/instance-welcome-view.tsx
@@ -404,6 +404,7 @@ const InstanceWelcomeView: Component = (props) => {
-
+
@@ -550,7 +550,7 @@ export default function MessageItem(props: MessageItemProps) {
- ⚠️ {errorMessage()}
+ ⚠️ {errorMessage()}
diff --git a/packages/ui/src/components/message-part.tsx b/packages/ui/src/components/message-part.tsx
index 038b3dfb..35db0b98 100644
--- a/packages/ui/src/components/message-part.tsx
+++ b/packages/ui/src/components/message-part.tsx
@@ -133,11 +133,12 @@ export default function MessagePart(props: MessagePartProps) {
- {plainTextContent()}}>
+ {plainTextContent()}}>
= (props) => {
{rowProps.isChild ? : }
- {title()}
+ {title()}
)}
diff --git a/packages/ui/src/components/tool-call/markdown-render.tsx b/packages/ui/src/components/tool-call/markdown-render.tsx
index 17142718..6bf55c48 100644
--- a/packages/ui/src/components/tool-call/markdown-render.tsx
+++ b/packages/ui/src/components/tool-call/markdown-render.tsx
@@ -43,7 +43,7 @@ export function createMarkdownContentRenderer(params: {
ref={registerRef}
onScroll={disableScrollTracking ? undefined : params.scrollHelpers.handleScroll}
>
-
{options.content}
+
{options.content}
{params.scrollHelpers.renderSentinel({ disableTracking: disableScrollTracking })}
)
diff --git a/packages/ui/src/styles/markdown.css b/packages/ui/src/styles/markdown.css
index d8b89be7..8cd10e07 100644
--- a/packages/ui/src/styles/markdown.css
+++ b/packages/ui/src/styles/markdown.css
@@ -24,6 +24,21 @@
color: inherit;
}
+ /* Auto-detect text direction per block element for RTL language support (e.g. Hebrew, Arabic) */
+ .markdown-body p,
+ .markdown-body li,
+ .markdown-body h1,
+ .markdown-body h2,
+ .markdown-body h3,
+ .markdown-body h4,
+ .markdown-body h5,
+ .markdown-body h6,
+ .markdown-body blockquote,
+ .markdown-body td,
+ .markdown-body th {
+ unicode-bidi: plaintext;
+ }
+
.markdown-body h1,
.markdown-body h2,
.markdown-body h3,
@@ -129,16 +144,19 @@
}
.markdown-body blockquote {
- border-left: 3px solid var(--border-base);
+ border-inline-start: 3px solid var(--border-base);
color: var(--text-secondary);
background-color: var(--surface-muted);
padding: 0.5rem 1rem;
- border-radius: 0 8px 8px 0;
+ border-start-start-radius: 0;
+ border-start-end-radius: 8px;
+ border-end-end-radius: 8px;
+ border-end-start-radius: 0;
}
.markdown-body ul,
.markdown-body ol {
- padding-left: 1.5rem;
+ padding-inline-start: 1.5rem;
margin: 0.5rem 0;
}
@@ -166,7 +184,7 @@
.markdown-body td {
border: 1px solid var(--border-base);
padding: 0.5rem 0.75rem;
- text-align: left;
+ text-align: start;
color: var(--text-primary);
background-color: transparent;
}
@@ -221,7 +239,7 @@
cursor: pointer;
color: var(--text-secondary);
transition: background-color 150ms ease, color 150ms ease, border-color 150ms ease;
- margin-left: auto;
+ margin-inline-start: auto;
font-size: var(--font-size-sm);
}