From 538fcae70880f43ebd5c7c7b106bb339b0e15ab2 Mon Sep 17 00:00:00 2001 From: Codeman maintainer Date: Sun, 25 Oct 2026 16:03:41 +0100 Subject: [PATCH 1/3] fix(response-viewer): lay out Persian, Arabic and Hebrew right to left (#715) The Response Viewer and the File Viewer's markdown view gave no block a dir, so a right-to-left answer was left-aligned with its punctuation on the wrong end and its bullets and quote bar on the left. _renderMarkdown now gives each outermost prose block dir="auto" (nested blocks inherit, so a list keeps its markers on one side), pins pre and code to dir="ltr", and keeps a dir the agent wrote. _linkifyFilePaths pins the path anchors it creates to ltr. Because dir="auto" skips descendants that carry a dir, code and paths no longer decide a sentence's direction. The list indent, quote bar and table borders move to logical properties, and the phone Compose textarea gets dir="auto". The terminal pane is unchanged: xterm.js has no right-to-left support (xtermjs/xterm.js#701). Co-Authored-By: Claude Opus 5.5 (1M context) --- .changeset/rtl-rendered-markdown.md | 5 + src/web/public/app.js | 26 ++++ src/web/public/keyboard-accessory.js | 4 +- src/web/public/styles.css | 16 ++- test/response-viewer-rtl.test.ts | 189 +++++++++++++++++++++++++++ 5 files changed, 233 insertions(+), 7 deletions(-) create mode 100644 .changeset/rtl-rendered-markdown.md create mode 100644 test/response-viewer-rtl.test.ts diff --git a/.changeset/rtl-rendered-markdown.md b/.changeset/rtl-rendered-markdown.md new file mode 100644 index 00000000..0b73d74b --- /dev/null +++ b/.changeset/rtl-rendered-markdown.md @@ -0,0 +1,5 @@ +--- +'aicodeman': patch +--- + +Persian, Arabic and Hebrew now read right to left in the Response Viewer and in the File Viewer's markdown view (#715). Each paragraph, heading, list, quote and table takes its direction from its own first letter, so a right-to-left answer is right-aligned with its punctuation, list bullets and quote bar on the right, while an English paragraph beside it stays left to right. Code blocks, inline code and file paths stay left to right and no longer decide a sentence's direction, so a Persian sentence that starts with `npm install` still reads right to left. The phone's Compose dialog lays a right-to-left prompt out the same way. The terminal pane is unchanged: xterm.js has no right-to-left support yet (xtermjs/xterm.js#701). diff --git a/src/web/public/app.js b/src/web/public/app.js index db655131..7a3e52bd 100644 --- a/src/web/public/app.js +++ b/src/web/public/app.js @@ -2835,6 +2835,29 @@ class CodemanApp { a.setAttribute('target', '_blank'); a.setAttribute('rel', 'noopener noreferrer'); }); + // Right-to-left text (issue #715). With no `dir`, every block was laid + // out left to right, so a Persian, Arabic or Hebrew answer was + // left-aligned, its punctuation landed on the wrong end and its lists + // and quotes hung off the left edge. Each OUTERMOST prose block now + // takes its direction from its own first letter (`dir="auto"`, what + // GitHub does for rendered markdown), so an RTL paragraph beside an + // English one reads correctly in both; nested blocks inherit, which + // keeps a list's markers and indent on one side. A `dir` the agent + // wrote (the sanitizer allows it) is kept, and so is what it covers. + // + // Code and preformatted text are always left to right. `dir="ltr"` + // also isolates them, and `dir="auto"` skips any descendant that + // carries a `dir`, so `npm install` at the head of a Persian sentence + // no longer turns the whole sentence left to right. + tmpl.content + .querySelectorAll('p, h1, h2, h3, h4, h5, h6, ul, ol, dl, blockquote, .rv-table-wrap') + .forEach((el) => { + if (el.parentElement?.closest('[dir]') || el.hasAttribute('dir')) return; + el.setAttribute('dir', 'auto'); + }); + tmpl.content.querySelectorAll('pre, code').forEach((el) => { + if (!el.hasAttribute('dir')) el.setAttribute('dir', 'ltr'); + }); return tmpl.innerHTML; } catch { /* fall through */ } } @@ -3064,6 +3087,9 @@ class CodemanApp { link.dataset.path = path; link.dataset.linkified = '1'; if (folder) link.dataset.folder = '1'; + // A path reads left to right, and in a right-to-left sentence it must + // not vote on the sentence's direction (`_renderMarkdown`, issue #715). + link.dir = 'ltr'; link.title = path; link.textContent = path; frag.appendChild(link); diff --git a/src/web/public/keyboard-accessory.js b/src/web/public/keyboard-accessory.js index 7ae3213c..abfcf8f1 100644 --- a/src/web/public/keyboard-accessory.js +++ b/src/web/public/keyboard-accessory.js @@ -1296,7 +1296,7 @@ const KeyboardAccessoryBar = { Compose prompt Enter adds a new line - +
@@ -1309,6 +1309,8 @@ const KeyboardAccessoryBar = { this._composerOverlay = overlay; const textarea = overlay.querySelector('.prompt-composer-textarea'); + // The textarea's dir="auto" lays a prompt written in Persian, Arabic or + // Hebrew out right to left, from its first letter (#715). // i18n.js skips