diff --git a/CHANGELOG.md b/CHANGELOG.md index d6afd3f..d13ad96 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## v0.5.1 + +- **Per-block direction detection (Auto mode)** — response paragraphs previously picked their direction from the *first* character (`unicode-bidi: plaintext`), so a Hebrew answer with a line starting in English drifted left, list bullets flipped to the wrong side, and mixed Hebrew/English lines were hard to read. Auto mode now decides direction per block element (paragraph, list item, heading, blockquote) by *presence* of Hebrew/Arabic/Persian characters: mixed lines align right and read correctly with bullets on the right, while pure-English headings and lines stay naturally left-aligned. Applied live during streaming; code blocks, tool output, and thinking blocks are untouched. + ## v0.5.0 - **New: Force LTR (Always) mode** (`Claude RTL: Force LTR (Always)`) — pins the whole chat (messages, input box, question/permission dialogs, Plan Preview) to left-to-right, even when the conversation contains Hebrew, Arabic, or Persian text. The direction choice is now symmetric: users who want RTL pick an RTL mode, users who prefer a stable LTR layout while chatting in an RTL language pick LTR Always. Shown in the status bar as `LTR: Always` and available from the status-bar menu; survives Claude Code updates via auto-reactivate like the other modes. (Contributed by @moeseif in #19.) diff --git a/docs/superpowers/specs/2026-08-04-auto-mode-per-block-direction-design.md b/docs/superpowers/specs/2026-08-04-auto-mode-per-block-direction-design.md new file mode 100644 index 0000000..969c10e --- /dev/null +++ b/docs/superpowers/specs/2026-08-04-auto-mode-per-block-direction-design.md @@ -0,0 +1,71 @@ +# Auto Mode: Per-Block Direction Detection + +**Date:** 2026-08-04 +**Status:** Approved +**Scope:** `src/content.ts` only (Auto mode CSS + JS) + +## Problem + +In Auto mode, once a bubble is tagged `.YBYrtl`, response paragraphs get +`unicode-bidi: plaintext`, which sets each paragraph's base direction from its +**first strong character**. In a Hebrew conversation this causes: + +1. Paragraphs that start with an English word flip to LTR and align left. +2. List markers (bullets/dots) follow that direction and land on the wrong side. +3. Mixed Hebrew/English lines scramble punctuation and word order, hard to read. + +## Decision + +Direction is decided **per block element** by *presence* of RTL characters, not +by first character: + +- Block **contains** Hebrew/Arabic/Persian → `dir="rtl"` → right-aligned, + bullet on right, embedded English isolated inline. +- Block has **no** RTL characters (pure English heading, free-standing code-ish + line) → `dir="ltr"` → left-aligned, natural. + +User-approved trade-off: a lone English word on its own line goes left; a Hebrew +paragraph containing lots of English still goes right. + +## Mechanism + +### JS (extends the existing Auto-mode observer in `RTL_AUTO_JS_CODE`) + +- For each `.YBYrtl` bubble, walk block elements — `p`, `li`, `h1`–`h6`, + `blockquote` — **only inside markdown containers** (`[class*="root_"]`); + tool/thinking/todo UI reuses the same tags and must not be tagged. +- Additionally skip anything inside every container the LTR overrides protect: + `pre`, `code`, `codeBlockWrapper_`, `thinking_`/`thinkingContent_`, + `toolUse_`/`toolSummary_`/`toolBody_`/`toolResult_`/`toolReference_`, + `todoList_`/`todoListContainer_` — a native `dir` attribute on a child is not + neutralized by direction rules on its container. +- Set `dir="rtl"` or `dir="ltr"` per the RTL-char test + (`/[--ۿݐ-ݿﭐ-﷿ﹰ-]/`). +- Re-scan on mutations, debounced — same pattern as the BiDi stripper — so a + streamed line that starts in English flips right once Hebrew arrives. + +### CSS (in `AUTO_RTL_RULES`) + +- Remove `unicode-bidi: plaintext` and the blanket `text-align: right` from + response-paragraph rules. +- Add: + - `[dir="rtl"]` blocks → `direction: rtl; text-align: right; unicode-bidi: isolate`. + - `[dir="ltr"]` blocks → `direction: ltr; text-align: left`. +- Bubble-level `direction: rtl` layout rules stay (bubble alignment unchanged). +- Prompt input rules stay as-is (first-char live detection is correct while + typing). + +## Out of Scope + +- Active / Always / LTR-Always modes (their semantics are explicit, not + detected). +- Plan Preview. +- Input field. + +## Verification + +- `npm run build` clean; `npm test` (concurrency) still passes. +- Manual: in Auto mode with a mixed Hebrew/English response confirm — + English-first mixed line right-aligned and readable, bullets on the right, + pure-English heading stays left, code blocks untouched, streaming reply + settles correctly. diff --git a/package-lock.json b/package-lock.json index e277e89..48da0c7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "rtl-support", - "version": "0.5.0", + "version": "0.5.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "rtl-support", - "version": "0.5.0", + "version": "0.5.1", "license": "MIT", "devDependencies": { "@types/node": "^20.0.0", diff --git a/package.json b/package.json index a999e79..d676692 100644 --- a/package.json +++ b/package.json @@ -2,7 +2,7 @@ "name": "rtl-support", "displayName": "RTL Support", "description": "Adds RTL (Right-to-Left) text support for Hebrew, Arabic and Persian to Claude Code in VS Code, Cursor, Antigravity and Kiro", - "version": "0.5.0", + "version": "0.5.1", "publisher": "Digitizers", "license": "MIT", "homepage": "https://github.com/Digitizers/vs-code-rtl-extension#readme", @@ -117,7 +117,7 @@ "build": "node esbuild.mjs --production", "watch": "node esbuild.mjs --watch", "pretest": "tsc -p tsconfig.test.json", - "test": "node test/concurrency.test.cjs", + "test": "node test/concurrency.test.cjs && node test/dir-walker-scope.test.cjs", "package": "npx @vscode/vsce package" }, "devDependencies": { diff --git a/src/content.ts b/src/content.ts index cd77f5e..8ca0a61 100644 --- a/src/content.ts +++ b/src/content.ts @@ -294,17 +294,27 @@ const AUTO_RTL_RULES = ` unicode-bidi: plaintext; } -/* Claude's markdown responses (excluding thinking block) */ +/* Claude's markdown responses (excluding thinking block) — container stays RTL + for bubble layout; per-block direction is set by the Auto-mode block walker + via dir attributes (contains-RTL → rtl, pure LTR → ltr) */ .YBYrtl [class*="root_"]:not([class*="thinkingContent_"] [class*="root_"]) { direction: rtl; - unicode-bidi: plaintext; } -.YBYrtl [class*="root_"]:not([class*="thinkingContent_"] [class*="root_"]) > :is(p, ul, ol, h1, h2, h3, h4, blockquote), -.YBYrtl [class*="root_"]:not([class*="thinkingContent_"] [class*="root_"]) > :is(ul, ol) li { +.YBYrtl [class*="root_"]:not([class*="thinkingContent_"] [class*="root_"]) :is(p, li, h1, h2, h3, h4, h5, h6, blockquote)[dir="rtl"] { + direction: rtl; text-align: right; + unicode-bidi: isolate; } +.YBYrtl [class*="root_"]:not([class*="thinkingContent_"] [class*="root_"]) :is(p, li, h1, h2, h3, h4, h5, h6, blockquote)[dir="ltr"] { + direction: ltr; + text-align: left; + unicode-bidi: isolate; +} + +/* Links keep their own bidi run — unicode-bidi is not inherited, so block-level + isolation alone leaves URL punctuation reorderable by the RTL context */ .YBYrtl [class*="root_"]:not([class*="thinkingContent_"] [class*="root_"]) a { unicode-bidi: plaintext; } @@ -906,6 +916,131 @@ export const RTL_AUTO_JS_CODE = ` }, 50); }).observe(scanRoot, { childList: true, subtree: true, characterData: true }); })(); + +/* Per-Block Direction — sets dir="rtl"/"ltr" on markdown blocks inside .YBYrtl + bubbles by presence of RTL characters, so mixed lines read right-aligned + while pure-English blocks stay natural LTR. CSS keys off the dir attribute. */ +(function() { + var RTL = /[\\u0590-\\u05FF\\u0600-\\u06FF\\u0750-\\u077F\\uFB50-\\uFDFF\\uFE70-\\uFEFE]/; + var BLOCK_SEL = 'p,li,h1,h2,h3,h4,h5,h6,blockquote'; + /* Every container the LTR overrides protect — a native dir attribute on a + child is NOT neutralized by direction rules on the container, so the + walker must never tag inside these */ + var SKIP_SEL = '[class*="codeBlockWrapper_"],pre,code,[class*="thinkingContent_"],[class*="thinking_"],[class*="toolUse_"],[class*="toolSummary_"],[class*="toolBody_"],[class*="toolResult_"],[class*="toolReference_"],[class*="todoList_"],[class*="todoListContainer_"]'; + + /* Blocks that render independently (own marker / own alignment) — text + inside them must not influence an ancestor block's direction. p and + headings inside a loose
…