From 39d0e6c5ab7fe201ab15f76caedca168e08d1bb1 Mon Sep 17 00:00:00 2001 From: Codeman maintainer Date: Fri, 6 Nov 2026 04:49:02 +0100 Subject: [PATCH] docs: describe right-to-left rendering in the manual and the invariants (release review) The wiki gains right-to-left notes for Last Response, the phone's Compose editor and the rendered Markdown view, plus a Troubleshooting entry for the #715 report: the terminal is xterm.js, which has no right-to-left support (xtermjs/xterm.js#701). src/web/CLAUDE.md and the invariants now state the dir pins, the logical-property rule and that the direction pass runs last. The changeset claimed every file path stays left to right. It now says only what holds: the paths the viewer links on its own. Co-Authored-By: Claude Opus 5.5 --- .changeset/rtl-rendered-markdown.md | 2 +- docs/architecture-invariants.md | 2 +- docs/wiki/Mobile-Guide.md | 6 ++++-- docs/wiki/Troubleshooting.md | 14 ++++++++++++++ docs/wiki/Watching-Agents-Work.md | 4 +++- docs/wiki/Working-With-Files.md | 2 +- src/web/CLAUDE.md | 4 ++-- 7 files changed, 26 insertions(+), 8 deletions(-) diff --git a/.changeset/rtl-rendered-markdown.md b/.changeset/rtl-rendered-markdown.md index 0b73d74b..bf5807ef 100644 --- a/.changeset/rtl-rendered-markdown.md +++ b/.changeset/rtl-rendered-markdown.md @@ -2,4 +2,4 @@ '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). +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 the file paths the viewer links on its own 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/docs/architecture-invariants.md b/docs/architecture-invariants.md index 479d279b..a029298a 100644 --- a/docs/architecture-invariants.md +++ b/docs/architecture-invariants.md @@ -403,7 +403,7 @@ A file path an agent prints is a link on both surfaces it can appear on, and cli **One pattern, two consumers.** `FILE_PATH_LINK_PATTERN` / `absoluteFilePathPattern()` live in `constants.js`; the xterm link provider (`registerFilePathLinkProvider`, terminal-ui.js) and the response viewer's `_linkifyFilePaths()` (app.js) both reach it through `findFilePathLinks()` (below), which builds a fresh instance per call. ⚠️ Fresh per call, never one shared object: `lastIndex` is per-object state on a `/g` regex. The pattern is anchored on a known absolute root and terminated by a known extension, so a fraction (`3/4`) or a date can't match and trailing punctuation stays out. Roots include `Users` and `mnt`, without which nothing was clickable on macOS or WSL. The linear-time guard and the "terminal-ui builds from the factory" structural check are in `test/link-provider-regex.test.ts`. -**The chat linkifier walks text nodes.** `_linkifyFilePaths()` builds anchors with `createElement`/`textContent` on the rendered subtree, never by rebuilding sanitized markup as a string — the source is model output. Subtrees already inside an `` are skipped (marked autolinks URLs; a nested anchor would swallow the click), and the anchor's text is the path verbatim so "copy code" still yields what the agent printed. Before the walk it also rewrites existing anchors whose href is a scheme-less, non-`//` path (Codex cites files as `[label](/abs/path.json)`, which otherwise opens as a same-origin URL) into the same `a.rv-path` shape, plus `rv-path-link` so the label keeps the prose font, on purpose without `FILE_PATH_LINK_PATTERN`'s root and extension filter: the href is a link target the agent chose. `test/response-viewer-file-links.test.ts` pins both properties and the anchor rewrite. +**The chat linkifier walks text nodes.** `_linkifyFilePaths()` builds anchors with `createElement`/`textContent` on the rendered subtree, never by rebuilding sanitized markup as a string — the source is model output. Subtrees already inside an `` are skipped (marked autolinks URLs; a nested anchor would swallow the click), and the anchor's text is the path verbatim so "copy code" still yields what the agent printed. Before the walk it also rewrites existing anchors whose href is a scheme-less, non-`//` path (Codex cites files as `[label](/abs/path.json)`, which otherwise opens as a same-origin URL) into the same `a.rv-path` shape, plus `rv-path-link` so the label keeps the prose font, on purpose without `FILE_PATH_LINK_PATTERN`'s root and extension filter: the href is a link target the agent chose. The anchors the walk builds carry `dir="ltr"`, so a path at the head of a right-to-left sentence does not decide that sentence's direction; a rewritten link keeps no `dir`, since its label is prose (#715). ⚠️ `_applyBlockDirection()` runs after the linkifier in both consumers, never before it: a block already carrying `dir="auto"` re-resolves its direction on every text node replaced under it. `test/response-viewer-file-links.test.ts` pins both properties and the anchor rewrite, `test/response-viewer-rtl.test.ts` the direction and the order. **Out-of-workspace paths go through the attachment routes, not the file routes.** `file-content`/`file-raw` resolve against `workingDir` and 404 anything that escapes it, which is correct and unchanged — but the paths agents most often print (a `/tmp` capture, Claude's own scratchpad, another checkout) are exactly that, so clicking one used to report "File not found" for a file sitting on disk. `openFilePreview()` now detects the case (`_isExternalPreviewPath`, a string compare for ROUTING only; the real decision stays server-side) and registers the path via `POST /api/sessions/:id/attachments` first, rendering by id. ⚠️ That registration passes `notify: false`, which suppresses ONLY the `attachment:detected` broadcast — the guard, the registry entry and the by-id routes are identical either way. Without it every click also popped an attachment card announcing the file already filling the screen. ⚠️ The click is an explicit user action on the **explicit, Origin-guarded** registration route, which is why it may cross the workspace boundary at all; the passive magic-link scanner stays force-confined. A type outside `SUPPORTED_ATTACHMENT_EXTENSIONS` (`.svg`, `.bmp`) is refused with a message naming what IS previewable, rather than the registry's own policy term. diff --git a/docs/wiki/Mobile-Guide.md b/docs/wiki/Mobile-Guide.md index c090c6bf..d494864b 100644 --- a/docs/wiki/Mobile-Guide.md +++ b/docs/wiki/Mobile-Guide.md @@ -66,8 +66,9 @@ A row of keys above the virtual keyboard, and what it contains depends on the se **Agent sessions** get quick actions: `/init`, `/clear`, `/compact`, a Compose key, `Esc`, a path picker, and 🧠 when Read My Mind is on. Compose opens a multiline editor with autocorrect: Enter adds a new line, and only Send delivers the text, as one paste followed -by Enter, so your line breaks reach the agent intact. Anything already typed on the terminal -prompt moves into the editor when it opens. Drafts are kept per session and in memory only, +by Enter, so your line breaks reach the agent intact. A prompt written in Persian, Arabic or +Hebrew lays out right to left. Anything already typed on the terminal prompt moves into the +editor when it opens. Drafts are kept per session and in memory only, so switching tabs keeps them and a page reload forgets them; a dot on the key shows a draft is parked. The editor's Attach button attaches photos, videos or documents and puts their paths into the draft. @@ -187,6 +188,7 @@ Setup in [Notifications And Approvals](Notifications-And-Approvals). The terminal viewport is small. **Last Response** (opt-in header button) renders the agent's last answer as scrollable text instead, with a **More** button for additional context. +Persian, Arabic and Hebrew answers read right to left there. The [File Viewer](Working-With-Files) works on phones too, including edit mode, which is enough to fix a typo an agent introduced while you are away from your desk. diff --git a/docs/wiki/Troubleshooting.md b/docs/wiki/Troubleshooting.md index 5a8fba2e..99cb093d 100644 --- a/docs/wiki/Troubleshooting.md +++ b/docs/wiki/Troubleshooting.md @@ -246,6 +246,20 @@ If you are sending input over the API instead, your payload must end with `\r` o is ever sent. The request still succeeds and the text sits unsubmitted in the composer. See [Driving Codeman From An Agent](Driving-Codeman-From-An-Agent). +### Persian, Arabic or Hebrew looks wrong in the terminal + +The terminal is xterm.js, which does not support right-to-left text yet +([xtermjs/xterm.js#701](https://github.com/xtermjs/xterm.js/issues/701)). It puts each +character in its own cell from left to right, so a sentence comes out reversed, and the +default WebGL renderer also draws the letters unjoined. Codeman cannot fix that without +replacing the terminal. + +Read the answer in **Last Response** instead, where it lays out right to left, or open a +Markdown file in the [File Viewer](Working-With-Files). On desktop, turning off **WebGL +Renderer** in **App Settings → Terminal & Input** and reloading joins the letters, so each +word reads correctly, but the words of a sentence still run left to right. Phones already +use that renderer. + ## Mobile ### The keyboard covers the terminal, or scroll position jumps diff --git a/docs/wiki/Watching-Agents-Work.md b/docs/wiki/Watching-Agents-Work.md index e746acf9..79fd2da2 100644 --- a/docs/wiki/Watching-Agents-Work.md +++ b/docs/wiki/Watching-Agents-Work.md @@ -77,7 +77,9 @@ Turning on either starts the watcher. **Last Response** (header button, opt-in) renders the agent's last answer as scrollable text rather than terminal output. It exists mostly for phones, where reading a long answer in a -terminal viewport is painful. **More** loads additional context. +terminal viewport is painful. **More** loads additional context. Persian, Arabic and Hebrew +answers read right to left here, which the terminal cannot do, while code and the file paths +it links stay left to right. ## After the fact diff --git a/docs/wiki/Working-With-Files.md b/docs/wiki/Working-With-Files.md index 9486ac69..79086e3d 100644 --- a/docs/wiki/Working-With-Files.md +++ b/docs/wiki/Working-With-Files.md @@ -14,7 +14,7 @@ It renders what it can: | Kind | Behaviour | | ------------------------ | ------------------------------------------------------------------------- | | Text and code | Plain preview with Lines (line numbers) and Wrap toggles in the header. Long files are truncated in plain preview. | -| Markdown | Rendered by default: headings, tables, code blocks with copy buttons, images and links relative to the file (root-relative ones resolve from the workspace root, as on GitHub). Links to another heading of the same file (`[Install](#installation)`) scroll to it, with GitHub's heading names (lower-case, punctuation dropped, repeats numbered `-1`, `-2`), and never leave the page. Opened from an attachment card, where the file's folder is unknown, relative images show their alt text and relative links show as plain text. The MD pill in the header flips to source. | +| Markdown | Rendered by default: headings, tables, code blocks with copy buttons, images and links relative to the file (root-relative ones resolve from the workspace root, as on GitHub). Links to another heading of the same file (`[Install](#installation)`) scroll to it, with GitHub's heading names (lower-case, punctuation dropped, repeats numbered `-1`, `-2`), and never leave the page. Opened from an attachment card, where the file's folder is unknown, relative images show their alt text and relative links show as plain text. Persian, Arabic and Hebrew read right to left, each block following its own first letter, while code stays left to right. The MD pill in the header flips to source. | | Images | Inline. | | Audio and video | Inline with a working scrub bar, because range requests are supported. | | Spreadsheets (`.xlsx`) | Read-only grid, parsed in your browser (never on the server), up to 10 MB. `.xls` and `.ods` are download only. | diff --git a/src/web/CLAUDE.md b/src/web/CLAUDE.md index 7f5ab0bf..dec9988a 100644 --- a/src/web/CLAUDE.md +++ b/src/web/CLAUDE.md @@ -22,7 +22,7 @@ **Ctrl+V paste trap** (`image-input.js`): `Ctrl+V` routes through `_handleImagePaste()`, which focuses a hidden `contenteditable` trap and reads the clipboard from the paste event landing there; images upload and their paths are typed in, plain text is a Codeman-built bracketed paste handed to that pane's terminal via `terminal.input(framed)` (strip nested 200/201 markers, newlines to `\r`, wrap CSI 200~/201~) because `terminal.paste()` drops those markers after a replay clears xterm's DECSET 2004 mirror (#716). `input()` fires onData as user input, so the main pane's bracketed-paste branch and a tile's `_onTerminalData` deliver the frame, including the local-echo flush (#219). The terminal is only refocused. ⚠️ **The trap must consume exactly ONE paste event** (Firefox delivers two per keypress: the `execCommand('paste')` event and the keydown's default action); the one-shot flag lives on the trap, never on a browser check. ⚠️ Do not remove the `execCommand('paste')` call: on some mobile engines it is the only route into the trap, and the trap is the only place image blobs are read. Tests: `test/image-paste-trap.test.ts`. → [architecture-invariants#terminal-paste-ctrlv](../../docs/architecture-invariants.md#terminal-paste-ctrlv) -**File Viewer text view: rendered markdown + Lines/Wrap toggles** (`_renderFilePreviewText()` in panels-ui.js): a `.md`/`.markdown` opens RENDERED by default with an `MD` pill back to source; the plain-text view has `Lines` (CSS-counter gutter) and `Wrap` toggles. ⚠️ ONE markdown pipeline: the viewer calls `_renderMarkdown(text, { breaks: false })` (marked + the DOMPurify allowlist, the Response Viewer's; chat keeps the default `breaks: true`, a file must not turn every hard wrap into a `
`) and binds the Response Viewer's click delegate (`_bindResponseViewerInteractions`) on the preview body for code-copy buttons and path links; never a second parser or handler. ⚠️ The document is built inside a `