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) <noreply@anthropic.com>
This commit is contained in:
Codeman maintainer committed 2026-10-25 16:03:41 +01:00
1 parent 3e06f3c141
commit 538fcae708
5 files changed
+233 -7

No files matched your search

+26
View File
@@ -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);
+3 -1
View File
@@ -1296,7 +1296,7 @@ const KeyboardAccessoryBar = {
<strong>Compose prompt</strong>
<span>Enter adds a new line</span>
</div>
<textarea class="paste-textarea prompt-composer-textarea" aria-label="Prompt" placeholder="Write your prompt…" autocorrect="on" autocapitalize="sentences" spellcheck="true"></textarea>
<textarea class="paste-textarea prompt-composer-textarea" dir="auto" aria-label="Prompt" placeholder="Write your prompt…" autocorrect="on" autocapitalize="sentences" spellcheck="true"></textarea>
<div class="paste-actions prompt-composer-actions">
<button type="button" class="prompt-composer-terminal">Use terminal keyboard</button>
<button type="button" class="paste-image">📎 Attach</button>
@@ -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 <textarea> subtrees (what is typed there is user content),
// so the placeholder and label are translated here, when the dialog is built.
const i18n = typeof window !== 'undefined' ? window.CodemanI18n : undefined;
+10 -6
View File
@@ -15085,7 +15085,7 @@ html:is([data-skin="paper-gray"], [data-skin="solarized-light"], [data-skin="cat
.rv-text ul, .rv-text ol,
.response-viewer-body > ul, .response-viewer-body > ol {
margin: 0.6em 0;
padding-left: 1.5em;
padding-inline-start: 1.5em;
}
.rv-text li,
@@ -15097,12 +15097,16 @@ html:is([data-skin="paper-gray"], [data-skin="solarized-light"], [data-skin="cat
.rv-text blockquote,
.response-viewer-body > blockquote {
border-left: 3px solid var(--accent);
border-inline-start: 3px solid var(--accent);
background: rgba(var(--accent-rgb), 0.06);
margin: 0.8em 0;
padding: 0.5em 14px;
color: var(--text-dim);
border-radius: 0 6px 6px 0;
/* Logical corners: the bar sits on the start side, right-to-left too (#715). */
border-start-start-radius: 0;
border-end-start-radius: 0;
border-start-end-radius: 6px;
border-end-end-radius: 6px;
}
.rv-text strong,
@@ -15172,9 +15176,9 @@ html:is([data-skin="paper-gray"], [data-skin="solarized-light"], [data-skin="cat
.response-viewer-body > table td,
.rv-table-wrap th, .rv-table-wrap td {
border-bottom: 1px solid var(--border);
border-right: 1px solid var(--border);
border-inline-end: 1px solid var(--border);
padding: 8px 12px;
text-align: left;
text-align: start;
vertical-align: top;
white-space: normal;
}
@@ -15183,7 +15187,7 @@ html:is([data-skin="paper-gray"], [data-skin="solarized-light"], [data-skin="cat
.response-viewer-body > table th:last-child,
.response-viewer-body > table td:last-child,
.rv-table-wrap th:last-child, .rv-table-wrap td:last-child {
border-right: none;
border-inline-end: none;
}
.rv-text tr:last-child td,