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/session-rename-dialog.tsx b/packages/ui/src/components/session-rename-dialog.tsx index c40d2e52..f414f240 100644 --- a/packages/ui/src/components/session-rename-dialog.tsx +++ b/packages/ui/src/components/session-rename-dialog.tsx @@ -76,6 +76,7 @@ const SessionRenameDialog: Component = (props) => { inputRef = element }} type="text" + dir="auto" value={title()} onInput={(event) => setTitle(event.currentTarget.value)} placeholder={t("sessionRenameDialog.input.placeholder")} diff --git a/packages/ui/src/components/tool-call/ansi-render.tsx b/packages/ui/src/components/tool-call/ansi-render.tsx index 1c8ca1e1..9eeab936 100644 --- a/packages/ui/src/components/tool-call/ansi-render.tsx +++ b/packages/ui/src/components/tool-call/ansi-render.tsx @@ -88,7 +88,7 @@ export function createAnsiContentRenderer(params: { return (
-
+        
         {params.scrollHelpers.renderSentinel()}
       
) diff --git a/packages/ui/src/components/tool-call/diagnostics-section.tsx b/packages/ui/src/components/tool-call/diagnostics-section.tsx index 9e5983f6..b4b9057f 100644 --- a/packages/ui/src/components/tool-call/diagnostics-section.tsx +++ b/packages/ui/src/components/tool-call/diagnostics-section.tsx @@ -42,7 +42,7 @@ export function renderDiagnosticsSection( {entry.displayPath} :L{entry.line || "-"}:C{entry.column || "-"} - {entry.message} + {entry.message}
)} 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); }