# תיקון עברית (RTL) בחלון הצ'אט של Claude Code

## מה זה פותר

כשעובדים עם Claude Code בתוך עורך קוד (VS Code, Cursor, Windsurf, Antigravity), טקסט בעברית מוצג מיושר לשמאל ובכיוון שגוי. עברית מעורבבת באנגלית נהיית בלתי קריאה - כולל בתיבות השאלה (הפופאפים עם אפשרויות הבחירה) ובבולטים שמתחילים במילה באנגלית או בשם קובץ.

התיקון מוסיף לממשק של התוסף זיהוי כיוון אוטומטי לכל בלוק טקסט, לפי כלל פשוט: בלוק שמכיל עברית מיושר לימין, גם אם הוא מתחיל באנגלית או במספרים. בלוק שכולו אנגלית נשאר בשמאל. בלוקים של קוד לא נוגעים בהם. גם תיבת ההקלדה ותיבות השאלה מתוקנות.

## מה עושים

1. פותחים את Claude Code בתוך העורך.
2. נותנים לסוכן את הקובץ הזה (גוררים אותו לצ'אט או מדביקים את התוכן) וכותבים: **"בצע את ההוראות במסמך"**.
3. כשהסוכן מסיים: `Ctrl+Shift+P` (במק: `Cmd+Shift+P`), מקלידים `Reload Window` ובוחרים "Developer: Reload Window".
4. שולחים הודעה בעברית ורואים את ההבדל.

**חשוב לדעת:** כשהתוסף של Claude Code מתעדכן (קורה אוטומטית מדי כמה ימים), התיקון נמחק. פשוט נותנים לסוכן את המסמך הזה שוב. הפעולה בטוחה להרצה חוזרת, וגם מנקה אוטומטית גרסאות ישנות של התיקון אם הותקנו בעבר.

---

# Agent Instructions (for the Claude Code agent)

You are running inside a code editor (VS Code, Cursor, Windsurf, or Antigravity IDE). Your task: patch the Claude Code extension's chat webview so mixed Hebrew/English text renders with correct per-block direction.

## How it works

The extension's chat UI is a webview bundled at `<extension-folder>/webview/` with two files: `index.css` and `index.js`. The patch (v3) appends:

1. A CSS block to `index.css`: `unicode-bidi: plaintext` on prose elements as a fallback, plus `unicode-bidi: isolate` on anything carrying a `dir` attribute.
2. A JS block to `index.js` that:
   - Sets direction per block by an "any Hebrew => RTL" rule: an element containing Hebrew text gets `dir="rtl"` even if it starts with English or digits (a plain `dir="auto"` first-strong heuristic misaligns bullets like `file.md - <hebrew>`). Hebrew-free prose elements get `dir="auto"`.
   - Detects Hebrew by walking text nodes, so `div`/`span`/`label` blocks (question popups, option lists) are covered, not just paragraphs.
   - Pierces shadow DOM: hooks `Element.prototype.attachShadow`, injects the CSS into every shadow root, and observes mutations inside it (the question popup renders inside a shadow root that plain CSS/observers never reach).
   - Handles streaming: a block first rendered with English-only text is upgraded to RTL the moment Hebrew characters arrive (`characterData` observation).
   - Skips `pre`/`code` so code blocks always stay LTR.

Both blocks carry the marker `RTL-AUTO-PATCH-V3`. The scripts are idempotent: if the V3 marker exists in a file, that file is skipped. Older `RTL-AUTO-PATCH` v1/v2 blocks are stripped automatically before appending (they were always appended at the end of the file, so everything from the first marker comment onward is removed).

## Step 1: Locate the extension

Search these directories (only the ones that exist) for folders matching `anthropic.claude-code-*` that contain `webview/index.css`:

- Windows: `%USERPROFILE%\.vscode\extensions`, `%USERPROFILE%\.vscode-insiders\extensions`, `%USERPROFILE%\.cursor\extensions`, `%USERPROFILE%\.windsurf\extensions`, `%USERPROFILE%\.antigravity-ide\extensions`
- macOS / Linux: same folder names under `~` (e.g. `~/.vscode/extensions`, `~/.cursor/extensions`, `~/.antigravity-ide/extensions`)

If several versioned folders exist, patch all of them (old ones are usually pending deletion; patching them is harmless).

If nothing is found, stop and tell the user the Claude Code extension webview was not located, and show which directories you checked.

## Step 2: Apply the patch

**On Windows**, save the PowerShell script below (e.g. to the workspace or a temp folder) and run it directly (do NOT use `-ExecutionPolicy Bypass`; invoke the file as-is or run its contents in your PowerShell tool):

```powershell
# patch-claude-rtl.ps1  (v3)
# Injects automatic RTL/LTR text-direction support into the Claude Code
# extension webview (VS Code / Cursor / Windsurf / Antigravity IDE).
# Re-run after every extension update (updates install a fresh folder
# without the patch).
#
# v2 additions over v1:
#   - Pierces shadow DOM (question popups render inside attachShadow roots
#     that v1 never reached): hooks attachShadow, injects CSS into every
#     shadow root, and observes inside it.
#   - Detects Hebrew by text content, so div/span/label blocks get handled
#     too. Skips pre/code so code blocks stay LTR.
# v3 additions over v2:
#   - "Any Hebrew => RTL" rule: a block containing Hebrew is aligned RTL
#     even when it STARTS with English/numbers (dir="auto" first-strong
#     heuristic broke bullets like "file.md - <hebrew text>").
#   - Streaming upgrade: blocks tagged "auto" while empty/English get
#     upgraded to "rtl" the moment Hebrew text arrives.
#   - Removes older v1/v2 patch blocks from the files before appending,
#     so versions never conflict.
#
# Idempotent: marked with RTL-AUTO-PATCH-V3; re-running is always safe.

$ErrorActionPreference = 'Stop'
$marker = 'RTL-AUTO-PATCH-V3'
$anyMarker = '/* RTL-AUTO-PATCH'

$cssPatch = @'

/* RTL-AUTO-PATCH-V3 - auto direction per block (fallback if JS patch fails) */
p, li, h1, h2, h3, h4, h5, h6, blockquote, td, th, summary,
textarea, [contenteditable="true"] { unicode-bidi: plaintext; }
[dir="rtl"], [dir="ltr"], [dir="auto"] { unicode-bidi: isolate; }
'@

$jsPatch = @'

/* RTL-AUTO-PATCH-V3 - any-Hebrew=>RTL per block, incl. shadow DOM + div/span text (question popups) */
(function () {
  if (window.__rtlAutoPatchV3) return; window.__rtlAutoPatchV3 = true;
  var HEB = /[\u0590-\u05FF\u0600-\u06FF]/;
  var SEL = 'p,li,h1,h2,h3,h4,h5,h6,blockquote,td,th,summary,textarea,[contenteditable="true"]';
  var CSS = SEL + '{unicode-bidi:plaintext}' + '\n[dir="rtl"],[dir="ltr"],[dir="auto"]{unicode-bidi:isolate}';
  var seen = new WeakSet();
  function skip(el) { return el.closest && el.closest('pre,code'); }
  function tag(el, dir) { el.setAttribute('dir', dir); el.__rtlPatched = true; }
  function onHebrew(el) {
    if (!el || skip(el)) return;
    if (!el.hasAttribute('dir')) tag(el, 'rtl');
    else if (el.__rtlPatched && el.getAttribute('dir') === 'auto') tag(el, 'rtl');
    var a = el.parentElement;
    while (a) {
      if (a.__rtlPatched && a.getAttribute('dir') === 'auto') tag(a, 'rtl');
      a = a.parentElement;
    }
  }
  function markText(root) {
    try {
      var w = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, null);
      var n;
      while ((n = w.nextNode())) if (HEB.test(n.nodeValue)) onHebrew(n.parentElement);
    } catch (e) {}
  }
  function initEl(el) {
    if (el.matches && el.matches(SEL) && !el.hasAttribute('dir') && !skip(el))
      tag(el, HEB.test(el.textContent) ? 'rtl' : 'auto');
  }
  function apply(node) {
    if (!node) return;
    if (node.nodeType === 3) { if (HEB.test(node.nodeValue)) onHebrew(node.parentElement); return; }
    if (node.nodeType !== 1 && node.nodeType !== 9 && node.nodeType !== 11) return;
    if (node.nodeType === 1) { initEl(node); if (node.shadowRoot) watch(node.shadowRoot); }
    if (node.querySelectorAll) {
      node.querySelectorAll(SEL).forEach(initEl);
      node.querySelectorAll('*').forEach(function (el) { if (el.shadowRoot) watch(el.shadowRoot); });
    }
    markText(node);
  }
  function watch(root) {
    if (!root || seen.has(root)) return;
    seen.add(root);
    if (root.nodeType === 11 && root.host) {
      try { var st = document.createElement('style'); st.textContent = CSS; root.appendChild(st); } catch (e) {}
    }
    apply(root);
    new MutationObserver(function (muts) {
      for (var i = 0; i < muts.length; i++) {
        var m = muts[i];
        if (m.type === 'characterData') { apply(m.target); continue; }
        for (var j = 0; j < m.addedNodes.length; j++) apply(m.addedNodes[j]);
      }
    }).observe(root, { childList: true, subtree: true, characterData: true });
  }
  var orig = Element.prototype.attachShadow;
  if (orig) Element.prototype.attachShadow = function () {
    var r = orig.apply(this, arguments);
    try { watch(r); } catch (e) {}
    return r;
  };
  function start() { watch(document); }
  if (document.body) start();
  else window.addEventListener('DOMContentLoaded', start);
})();
'@

# Extension roots to scan (all editors that host the Claude Code extension)
$roots = @(
  "$env:USERPROFILE\.vscode\extensions",
  "$env:USERPROFILE\.vscode-insiders\extensions",
  "$env:USERPROFILE\.cursor\extensions",
  "$env:USERPROFILE\.windsurf\extensions",
  "$env:USERPROFILE\.antigravity-ide\extensions"
) | Where-Object { Test-Path $_ }

function Patch-File($path, $patch, $label, $dirName) {
  $enc = New-Object System.Text.UTF8Encoding($false)
  $raw = [IO.File]::ReadAllText($path)
  if ($raw -match $marker) { Write-Output "already patched $($label): $dirName"; return }
  $i = $raw.IndexOf($anyMarker)
  if ($i -ge 0) { $raw = $raw.Substring(0, $i).TrimEnd() }  # strip v1/v2 blocks
  [IO.File]::WriteAllText($path, $raw + $patch, $enc)
  Write-Output "PATCHED $($label): $dirName"
}

$found = $false
foreach ($root in $roots) {
  $dirs = Get-ChildItem -Path $root -Directory -Filter 'anthropic.claude-code-*' -ErrorAction SilentlyContinue
  foreach ($dir in $dirs) {
    $css = Join-Path $dir.FullName 'webview\index.css'
    $js  = Join-Path $dir.FullName 'webview\index.js'
    if (-not (Test-Path $css) -or -not (Test-Path $js)) { continue }
    $found = $true
    Patch-File $css $cssPatch 'CSS' $dir.Name
    Patch-File $js  $jsPatch  'JS ' $dir.Name
  }
}

if (-not $found) { Write-Output 'No Claude Code extension webview found - nothing patched.' }
else { Write-Output 'Done. Reload the editor window (Ctrl+Shift+P -> "Reload Window") to apply.' }
```

**On macOS / Linux**, run this bash equivalent instead:

```bash
#!/usr/bin/env bash
# patch-claude-rtl.sh (v3) - idempotent, safe to re-run after extension updates.
# Strips older v1/v2 patch blocks before appending v3.
set -euo pipefail
marker='RTL-AUTO-PATCH-V3'

css_patch='
/* RTL-AUTO-PATCH-V3 - auto direction per block (fallback if JS patch fails) */
p, li, h1, h2, h3, h4, h5, h6, blockquote, td, th, summary,
textarea, [contenteditable="true"] { unicode-bidi: plaintext; }
[dir="rtl"], [dir="ltr"], [dir="auto"] { unicode-bidi: isolate; }'

js_patch='
/* RTL-AUTO-PATCH-V3 - any-Hebrew=>RTL per block, incl. shadow DOM + div/span text (question popups) */
(function () {
  if (window.__rtlAutoPatchV3) return; window.__rtlAutoPatchV3 = true;
  var HEB = /[\u0590-\u05FF\u0600-\u06FF]/;
  var SEL = "p,li,h1,h2,h3,h4,h5,h6,blockquote,td,th,summary,textarea,[contenteditable=\"true\"]";
  var CSS = SEL + "{unicode-bidi:plaintext}" + "\n[dir=\"rtl\"],[dir=\"ltr\"],[dir=\"auto\"]{unicode-bidi:isolate}";
  var seen = new WeakSet();
  function skip(el) { return el.closest && el.closest("pre,code"); }
  function tag(el, dir) { el.setAttribute("dir", dir); el.__rtlPatched = true; }
  function onHebrew(el) {
    if (!el || skip(el)) return;
    if (!el.hasAttribute("dir")) tag(el, "rtl");
    else if (el.__rtlPatched && el.getAttribute("dir") === "auto") tag(el, "rtl");
    var a = el.parentElement;
    while (a) {
      if (a.__rtlPatched && a.getAttribute("dir") === "auto") tag(a, "rtl");
      a = a.parentElement;
    }
  }
  function markText(root) {
    try {
      var w = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, null);
      var n;
      while ((n = w.nextNode())) if (HEB.test(n.nodeValue)) onHebrew(n.parentElement);
    } catch (e) {}
  }
  function initEl(el) {
    if (el.matches && el.matches(SEL) && !el.hasAttribute("dir") && !skip(el))
      tag(el, HEB.test(el.textContent) ? "rtl" : "auto");
  }
  function apply(node) {
    if (!node) return;
    if (node.nodeType === 3) { if (HEB.test(node.nodeValue)) onHebrew(node.parentElement); return; }
    if (node.nodeType !== 1 && node.nodeType !== 9 && node.nodeType !== 11) return;
    if (node.nodeType === 1) { initEl(node); if (node.shadowRoot) watch(node.shadowRoot); }
    if (node.querySelectorAll) {
      node.querySelectorAll(SEL).forEach(initEl);
      node.querySelectorAll("*").forEach(function (el) { if (el.shadowRoot) watch(el.shadowRoot); });
    }
    markText(node);
  }
  function watch(root) {
    if (!root || seen.has(root)) return;
    seen.add(root);
    if (root.nodeType === 11 && root.host) {
      try { var st = document.createElement("style"); st.textContent = CSS; root.appendChild(st); } catch (e) {}
    }
    apply(root);
    new MutationObserver(function (muts) {
      for (var i = 0; i < muts.length; i++) {
        var m = muts[i];
        if (m.type === "characterData") { apply(m.target); continue; }
        for (var j = 0; j < m.addedNodes.length; j++) apply(m.addedNodes[j]);
      }
    }).observe(root, { childList: true, subtree: true, characterData: true });
  }
  var orig = Element.prototype.attachShadow;
  if (orig) Element.prototype.attachShadow = function () {
    var r = orig.apply(this, arguments);
    try { watch(r); } catch (e) {}
    return r;
  };
  function start() { watch(document); }
  if (document.body) start();
  else window.addEventListener("DOMContentLoaded", start);
})();'

strip_and_append() {
  f="$1"; patch="$2"; label="$3"; name="$4"
  if grep -q "$marker" "$f"; then echo "already patched $label: $name"; return; fi
  line=$(grep -n -m1 -F '/* RTL-AUTO-PATCH' "$f" | cut -d: -f1 || true)
  if [ -n "${line:-}" ]; then head -n $((line - 1)) "$f" > "$f.tmp" && mv "$f.tmp" "$f"; fi
  printf '%s\n' "$patch" >> "$f"
  echo "PATCHED $label: $name"
}

found=0
for root in "$HOME/.vscode/extensions" "$HOME/.vscode-insiders/extensions" \
            "$HOME/.cursor/extensions" "$HOME/.windsurf/extensions" \
            "$HOME/.antigravity-ide/extensions"; do
  [ -d "$root" ] || continue
  for dir in "$root"/anthropic.claude-code-*; do
    css="$dir/webview/index.css"; js="$dir/webview/index.js"
    [ -f "$css" ] && [ -f "$js" ] || continue
    found=1
    strip_and_append "$css" "$css_patch" "CSS" "$(basename "$dir")"
    strip_and_append "$js"  "$js_patch"  "JS " "$(basename "$dir")"
  done
done

if [ "$found" -eq 0 ]; then echo "No Claude Code extension webview found - nothing patched."
else echo 'Done. Reload the editor window (Ctrl+Shift+P -> "Reload Window") to apply.'; fi
```

## Step 3: Verify

Check that the tail of the active version's `webview/index.css` and `webview/index.js` now contains `RTL-AUTO-PATCH-V3`, and that older markers (`RTL-AUTO-PATCH v1`, `RTL-AUTO-PATCH-V2`) no longer appear anywhere in those files. Report which versions were patched.

## Step 4: Tell the user

Tell the user (in their language) to reload the editor window: `Ctrl+Shift+P` (`Cmd+Shift+P` on Mac) -> "Developer: Reload Window", then send a Hebrew message to see the fix. Also tell them: extension updates overwrite the patch, so they should hand you this document again whenever Hebrew alignment breaks.

## Safety notes

- Only touch the two webview files (`index.css`, `index.js`). Never modify, minify, or rewrite the original bundled content - the scripts only strip previously appended RTL-AUTO-PATCH blocks (always at the end of the file) and append the new one.
- If the V3 marker is already present, do nothing to that file.
- If the user's editor is not listed above, find its extensions directory (same `anthropic.claude-code-*` naming) and apply the same logic.
