Reproduce and Fix a Real Open-Source Bug

Diagnose and fix an Arabic RTL rendering bug in a real YC-backed IDE.

Introduction

30 Second Summary

Over 400 million people write in Arabic, yet many developer tools still break when they encounter right-to-left text. When a user types Arabic in their terminal and sees garbled, disconnected letters, that is a broken experience worth fixing.

In this project, you will reproduce and fix a real P1 bug in Orca IDE, a YC-backed open-source code editor with 10,000+ stars. You will clone the source, write an automated test that captures the bug, diagnose the root cause across rendering layers, and implement a verified fix.

What You'll Build

A complete bug-reproduction-to-fix pipeline for a real-world rendering bug where Arabic text appears reversed and disconnected in Orca's xterm.js-based terminal.

By the end of this project, you'll have:

  • A Playwright e2e test that reliably reproduces the Arabic RTL rendering bug in Orca's terminal.
  • A diagnostic instrumentation proving the PTY buffer is correct and isolating the rendering layer as the root cause.
  • A DOM overlay module with Vitest unit tests that fixes Arabic text rendering and passes the full CI suite.
  • Secret Mission: Extend the overlay to handle mixed bidirectional text with Arabic, English, and numbers on the same line.

Are there any prerequisites?

You should be comfortable with TypeScript basics, Git fundamentals (cloning, branching), and working in a terminal. No prior experience with Electron or RTL text handling is needed.

Set Up the Orca Development Environment

Before you can reproduce and fix a bug, you need a local copy of the project running on your machine. Orca is a large Electron-based TypeScript monorepo with native dependencies, so the setup involves cloning the source, installing packages (including compiled native modules), and verifying both the app and its test suites work.

By the end of this step you will have Orca running in development mode and know where the terminal rendering code lives. This gives you everything you need to start reproducing the bug in Step 2.

In this step, get ready to:
  • Clone the Orca repository and install all dependencies.
  • Verify the test infrastructure (Vitest and Playwright) works.
  • Locate the terminal rendering code you will modify later.
Clone the Orca repository and install dependencies

Orca's source code lives on GitHub. You will clone it locally, install its dependencies with pnpm, and confirm the dev build launches correctly.

  • Open your terminal.
  • Move to your Desktop by running this command:
cd ~/Desktop
  • Clone the Orca repository by running this command:
git clone https://github.com/stablyai/orca.git
  • Move into the cloned directory by running:
cd orca
  • Confirm the orca/ folder exists by running:
ls

You should see files like package.json, pnpm-lock.yaml, and directories like src/ and tests/ in the output.

  • Install all dependencies by running:
pnpm install

This takes a few minutes. It installs all packages listed in package.json including native modules like node-pty which require compilation on Linux.

What does this command do?

pnpm install reads the pnpm-lock.yaml lockfile and installs every dependency the project needs.

Native modules like node-pty don't ship prebuilt Linux binaries, so pnpm uses node-gyp to compile them from source. This is why you need a C/C++ toolchain on your system.

Seeing gyp ERR or build failures?

The most common cause is missing C/C++ build tools. node-pty requires make, gcc, and g++ to compile.

Install the build toolchain by running:

sudo apt-get install -y build-essential python3

Then retry with:

rm -rf node_modules
pnpm install

help me fix my pnpm install error in the Orca project

  • Once the install completes successfully, launch the dev build by running:
pnpm dev

After a moment, an Orca window should appear with a terminal pane ready for input.

  • Confirm the terminal works by typing any command (like echo hello) into the terminal pane.

Orca window not appearing?

On Linux, Electron needs a display server. Make sure you are running inside a graphical desktop session (not a headless SSH terminal).

If you see GPU-related warnings, Orca falls back to WebGL2 automatically. The app should still launch.

help me troubleshoot Orca not launching on Linux

  • Stop the dev server by pressing Ctrl+C in the terminal where you ran pnpm dev.
Verify the test infrastructure works

Orca uses Vitest for unit tests and Playwright for end-to-end tests. You need both working before you can write a test that reproduces the bug.

  • Run the unit test suite by running:
pnpm test

You should see Vitest output with all tests passing (green checkmarks).

What does pnpm test run?

Under the hood, pnpm test runs Vitest with the project's config at config/vitest.config.ts. It also runs a native-runtime check script first to ensure compiled modules like node-pty are loadable.

Tests failing or module not found errors?

If you see errors about missing native modules, your pnpm install may have partially failed. Re-run pnpm install and check for any error output in the install logs.

help me fix failing tests after pnpm install in Orca

  • Explore the end-to-end test directory by running:
ls tests/e2e/

You should see several .spec.ts files. These are Playwright tests that launch the full Electron app and interact with it.

  • Open any existing terminal-related test file to familiarize yourself with the test patterns. Look for files with "terminal" in the name.
  • Note the Playwright configuration at tests/playwright.config.ts. It defines an electron-headless project that runs e2e tests against Electron without a visible window.

Why e2e tests for a rendering bug?

A unit test cannot reproduce a rendering bug because it does not exercise the actual xterm canvas or WebGL renderer. End-to-end tests launch the full Electron app, which means the complete rendering pipeline is active.

This is why you will write a Playwright e2e test (not a Vitest unit test) to capture the reproduction in Step 2.

Locate the terminal rendering code

Now that you know the codebase builds and tests pass, you need to find the specific files that handle terminal rendering. This is where the bug lives.

  • Navigate to the pane manager directory by running:
ls src/renderer/src/lib/pane-manager/

You should see several TypeScript files related to terminal pane lifecycle management.

  • Identify pane-lifecycle.ts in the output. This file handles terminal setup, including where the terminal instance is created and attached to the DOM.
  • Note the terminal CSS file at src/renderer/src/assets/terminal.css. This is where you will add overlay styles later.
  • Open pane-lifecycle.ts in your text editor and scan for references to @xterm/xterm. This confirms Orca uses xterm.js (specifically the @xterm/xterm package) for terminal rendering.

What is xterm.js?

xterm.js is the terminal emulator library used by VS Code, Orca, and many other Electron-based editors. It renders terminal output cell-by-cell into a canvas (or WebGL surface).

This cell-by-cell rendering model is exactly what breaks Arabic text. Arabic script requires letters to connect contextually based on their position in a word, which a single-cell renderer cannot handle.

Your development environment is fully set up. Next up, you will read the bug report, reproduce the issue manually, and write an e2e test that captures it automatically.

Reproduce the Bug

You have a working Orca dev build. Now you need to prove the bug exists in a way that a machine can verify automatically.

Systematic bug reproduction means reading the report, confirming the failure manually, and then writing an automated test that captures it. The automated test becomes your proof: if it fails, the bug exists. If it passes, the bug is fixed.

In this step, get ready to:
  • Read the bug report and extract the exact reproduction steps.
  • Reproduce the bug manually in your Orca dev build.
  • Write a Playwright e2e test that captures the reproduction automatically.
Read the bug report and extract reproduction steps

Before touching any code, you need to understand exactly what the reporter saw and what they expected to see. Issue #5262 provides all the details.

Here is what the bug report tells you:

  • The bug: Arabic text typed or echoed in Orca's terminal appears with reversed word order and disconnected letters.
  • Expected input: مرحباً هذه مشكلة في اللغة العربية
  • Actual display: ابحرم هذه ةلكشم يف ةغللا ةيبرعلا (reversed and disconnected).
  • Reported on Orca v1.4.64, confirmed reproducible on macOS and Windows.
  • Key clue from the contributor diagnosis: "The PTY/xterm buffer receives the Arabic text correctly. The break happens in the terminal rendering path."

Why does this clue matter?

If the PTY buffer already has the correct text, the shell and transport layers are innocent. The problem is isolated to how xterm.js renders the characters on screen. This narrows your investigation to the rendering pipeline only.

Reproduce the bug manually in your dev build

Manual reproduction confirms the bug exists in your local environment. It also gives you a baseline to compare against after you implement the fix.

  • Switch back to your running Orca dev instance (the one you launched with pnpm dev in Step 1).
  • In the terminal pane, echo Arabic text by running:
echo "مرحباً هذه مشكلة"
  • Observe the output. The letters appear disconnected from each other (they should be joined in cursive form). The word order is reversed, reading left-to-right instead of right-to-left.
  • Open a standalone terminal (e.g., GNOME Terminal) and run the same command.
  • Compare: in GNOME Terminal the Arabic renders correctly with connected letters in right-to-left order. In Orca it does not.

What is happening?

Arabic is a cursive script. Letters change shape depending on whether they appear at the beginning, middle, or end of a word. Orca's terminal uses xterm.js which renders text cell-by-cell. This cell-by-cell model breaks both Arabic letter joining and right-to-left ordering.

This is a known limitation documented in xterm.js issue #701, open since 2017. Your standalone terminal does not use xterm.js, so it renders Arabic correctly using the operating system's native text engine.

Write a Playwright e2e test to capture the reproduction

Now you will automate what you just observed manually. The test describes the correct behavior (connected RTL text rendered via a DOM overlay). Since the fix does not exist yet, the test will fail. That failing test is your automated proof that the bug exists.

Why write the test before the fix?

This is test-driven development (TDD) applied to bug fixing. You write a test asserting what correct behavior looks like. It fails now because the behavior is broken. After you implement the fix, the same test passes. This gives you confidence the fix actually works and protects against regressions.

  • Create the file tests/e2e/terminal-arabic-rtl-rendering.spec.ts by running:
touch tests/e2e/terminal-arabic-rtl-rendering.spec.ts
  • Open tests/e2e/terminal-arabic-rtl-rendering.spec.ts and add the imports, test constant, and test suite setup:
import { test, expect } from '@playwright/test'
import { _electron as electron } from 'playwright'
import path from 'path'

const ARABIC_TEXT = 'مرحباً هذه مشكلة في اللغة العربية'

test.describe('Arabic RTL terminal rendering (#5262)', () => {
  let app: Awaited<ReturnType<typeof electron.launch>>

  test.beforeAll(async () => {
    app = await electron.launch({
      args: [path.join(__dirname, '../../out/main/index.js')],
      env: { ...process.env, NODE_ENV: 'test' },
    })
  })

  test.afterAll(async () => {
    await app.close()
  })
})

What does this code do?

  • electron.launch() starts the Orca Electron app from its compiled output at out/main/index.js.
  • ARABIC_TEXT holds the test string from the original bug report.
  • beforeAll launches the app once for all tests in the suite. afterAll closes it when done.
  • Inside the test.describe block (before the closing })), add the primary reproduction test:
  test('Arabic text renders with connected letters in RTL order', async () => {
    const window = await app.firstWindow()
    await window.waitForLoadState('domcontentloaded')

    // Wait for a terminal pane to be available
    const terminalContainer = window.locator('.xterm-screen')
    await terminalContainer.first().waitFor({ state: 'visible', timeout: 15000 })

    // Write Arabic text to the terminal via PTY
    // Use the xterm buffer to send text directly
    await window.evaluate((text) => {
      const terminal = document.querySelector('.xterm')
      if (!terminal) throw new Error('No terminal element found')
      // Access the terminal instance through the DOM element's xterm property
      const xtermInstance = (terminal as any).__xtermInstance
      if (xtermInstance) {
        xtermInstance.write(`echo "${text}"\r`)
      }
    }, ARABIC_TEXT)

    // Wait for render
    await window.waitForTimeout(500)

    // Check that the RTL overlay is present for rows with Arabic text
    const rtlOverlay = window.locator('.orca-terminal-rtl-overlay')
    await expect(rtlOverlay).toBeVisible()

    // Verify the overlay contains the Arabic text with correct shaping
    const overlayText = await rtlOverlay.textContent()
    expect(overlayText).toContain('مرحباً')

    // Verify the overlay has RTL direction
    const direction = await rtlOverlay.evaluate((el) => {
      const row = el.querySelector('.rtl-overlay-row')
      return row ? getComputedStyle(row).direction : null
    })
    expect(direction).toBe('rtl')
  })

What does this test assert?

The test describes the fixed behavior: a .orca-terminal-rtl-overlay element should appear, contain the Arabic text, and have direction: rtl applied. Since this overlay does not exist yet, the test will fail at the toBeVisible() assertion. That failure is your automated reproduction of the bug.

  • Below the first test (still inside the test.describe block), add a negative test to confirm English text does not trigger the overlay:
  test('English-only text does not trigger RTL overlay', async () => {
    const window = await app.firstWindow()

    await window.evaluate(() => {
      const terminal = document.querySelector('.xterm')
      const xtermInstance = (terminal as any).__xtermInstance
      if (xtermInstance) {
        xtermInstance.write('echo "hello world"\r')
      }
    })

    await window.waitForTimeout(500)

    // The overlay should not have any visible RTL rows for English text
    const rtlRows = window.locator('.orca-terminal-rtl-overlay .rtl-overlay-row')
    const count = await rtlRows.count()
    // Any visible rows should not contain English-only content
    for (let i = 0; i < count; i++) {
      const text = await rtlRows.nth(i).textContent()
      if (text && text.trim()) {
        expect(text).toMatch(/[\u0600-\u06FF\u0590-\u05FF]/)
      }
    }
  })

Why include a negative test?

A fix for RTL rendering should only activate when Arabic text is present. The negative test ensures the overlay never fires for English-only content, protecting against performance overhead and unintended side effects.

  • Below that test, add one more test to verify the overlay updates when the terminal scrolls:
  test('overlay updates on terminal scroll', async () => {
    const window = await app.firstWindow()

    // Write multiple lines of Arabic text
    for (let i = 0; i < 5; i++) {
      await window.evaluate((text) => {
        const terminal = document.querySelector('.xterm')
        const xtermInstance = (terminal as any).__xtermInstance
        if (xtermInstance) {
          xtermInstance.write(`echo "${text} ${i}"\r`)
        }
      }, ARABIC_TEXT)
    }

    await window.waitForTimeout(500)

    // Scroll the terminal
    await window.evaluate(() => {
      const terminal = document.querySelector('.xterm')
      const xtermInstance = (terminal as any).__xtermInstance
      if (xtermInstance) {
        xtermInstance.scrollToTop()
      }
    })

    await window.waitForTimeout(200)

    // Overlay should still be present after scroll
    const rtlOverlay = window.locator('.orca-terminal-rtl-overlay')
    await expect(rtlOverlay).toBeVisible()
  })

What does this test cover?

If the overlay only renders once and never updates, scrolling would show stale or missing Arabic text. This test writes multiple lines, scrolls to the top, and confirms the overlay is still visible after the scroll event.

  • Save tests/e2e/terminal-arabic-rtl-rendering.spec.ts.
  • Run the e2e test to confirm it fails (reproducing the bug) by running:
pnpm run test:e2e -- tests/e2e/terminal-arabic-rtl-rendering.spec.ts
  • You should see all three tests fail. The first test fails at the toBeVisible() assertion because .orca-terminal-rtl-overlay does not exist in the DOM yet.

Tests not running at all?

  • Make sure you ran pnpm install in Step 1. Playwright needs its browser binaries installed as part of the dependency setup.
  • Check that your Orca build output exists at out/main/index.js. If it does not, run pnpm build before running the e2e tests.
  • If you see a timeout error, increase the timeout value in the waitFor call. Some machines take longer to launch Electron.

help me debug why my Playwright e2e test is not running against the Orca Electron app

✔️ Awesome, I've got everything!

Great. Double check you have saved tests/e2e/terminal-arabic-rtl-rendering.spec.ts and confirmed all three tests fail.

ⓧ I'd like to double check the full code

Here is the complete test file. Compare it against yours line by line:

import { test, expect } from '@playwright/test'
import { _electron as electron } from 'playwright'
import path from 'path'

const ARABIC_TEXT = 'مرحباً هذه مشكلة في اللغة العربية'

test.describe('Arabic RTL terminal rendering (#5262)', () => {
  let app: Awaited<ReturnType<typeof electron.launch>>

  test.beforeAll(async () => {
    app = await electron.launch({
      args: [path.join(__dirname, '../../out/main/index.js')],
      env: { ...process.env, NODE_ENV: 'test' },
    })
  })

  test.afterAll(async () => {
    await app.close()
  })

  test('Arabic text renders with connected letters in RTL order', async () => {
    const window = await app.firstWindow()
    await window.waitForLoadState('domcontentloaded')

    // Wait for a terminal pane to be available
    const terminalContainer = window.locator('.xterm-screen')
    await terminalContainer.first().waitFor({ state: 'visible', timeout: 15000 })

    // Write Arabic text to the terminal via PTY
    // Use the xterm buffer to send text directly
    await window.evaluate((text) => {
      const terminal = document.querySelector('.xterm')
      if (!terminal) throw new Error('No terminal element found')
      // Access the terminal instance through the DOM element's xterm property
      const xtermInstance = (terminal as any).__xtermInstance
      if (xtermInstance) {
        xtermInstance.write(`echo "${text}"\r`)
      }
    }, ARABIC_TEXT)

    // Wait for render
    await window.waitForTimeout(500)

    // Check that the RTL overlay is present for rows with Arabic text
    const rtlOverlay = window.locator('.orca-terminal-rtl-overlay')
    await expect(rtlOverlay).toBeVisible()

    // Verify the overlay contains the Arabic text with correct shaping
    const overlayText = await rtlOverlay.textContent()
    expect(overlayText).toContain('مرحباً')

    // Verify the overlay has RTL direction
    const direction = await rtlOverlay.evaluate((el) => {
      const row = el.querySelector('.rtl-overlay-row')
      return row ? getComputedStyle(row).direction : null
    })
    expect(direction).toBe('rtl')
  })

  test('English-only text does not trigger RTL overlay', async () => {
    const window = await app.firstWindow()

    await window.evaluate(() => {
      const terminal = document.querySelector('.xterm')
      const xtermInstance = (terminal as any).__xtermInstance
      if (xtermInstance) {
        xtermInstance.write('echo "hello world"\r')
      }
    })

    await window.waitForTimeout(500)

    // The overlay should not have any visible RTL rows for English text
    const rtlRows = window.locator('.orca-terminal-rtl-overlay .rtl-overlay-row')
    const count = await rtlRows.count()
    // Any visible rows should not contain English-only content
    for (let i = 0; i < count; i++) {
      const text = await rtlRows.nth(i).textContent()
      if (text && text.trim()) {
        expect(text).toMatch(/[\u0600-\u06FF\u0590-\u05FF]/)
      }
    }
  })

  test('overlay updates on terminal scroll', async () => {
    const window = await app.firstWindow()

    // Write multiple lines of Arabic text
    for (let i = 0; i < 5; i++) {
      await window.evaluate((text) => {
        const terminal = document.querySelector('.xterm')
        const xtermInstance = (terminal as any).__xtermInstance
        if (xtermInstance) {
          xtermInstance.write(`echo "${text} ${i}"\r`)
        }
      }, ARABIC_TEXT)
    }

    await window.waitForTimeout(500)

    // Scroll the terminal
    await window.evaluate(() => {
      const terminal = document.querySelector('.xterm')
      const xtermInstance = (terminal as any).__xtermInstance
      if (xtermInstance) {
        xtermInstance.scrollToTop()
      }
    })

    await window.waitForTimeout(200)

    // Overlay should still be present after scroll
    const rtlOverlay = window.locator('.orca-terminal-rtl-overlay')
    await expect(rtlOverlay).toBeVisible()
  })
})

You now have automated proof that the bug exists. Next up, you will diagnose exactly which layer of the rendering pipeline is broken and determine the fix strategy.

Diagnose the Root Cause

You have a failing e2e test that proves the bug exists. That is the foundation of professional debugging.

Now comes the detective work: pinpointing exactly which layer of the rendering pipeline is broken. A terminal emulator has multiple layers between input and display, and jumping to a fix without isolating the faulty layer wastes time and risks fixing the wrong thing.

In this step, get ready to:
  • Instrument the PTY buffer to confirm the shell layer stores correct Arabic codepoints.
  • Isolate the rendering layer as the source of the breakage.
  • Determine the fix strategy based on your diagnosis.
Instrument at the PTY layer to confirm the buffer is correct

The first question to answer is: does the terminal even receive the correct Arabic text from the shell? If the PTY corrupts the data before it reaches the renderer, that is a completely different class of bug.

You will use the xterm.js buffer API to read what the terminal has stored internally, independent of how it renders on screen.

  • Switch back to your running dev instance of Orca.
  • Open DevTools by pressing Ctrl+Shift+I inside the Orca window.
  • In the Console tab, type Arabic text into the terminal first by running:
echo "مرحباً هذه مشكلة في اللغة العربية"
  • Now read the buffer contents by pasting this diagnostic snippet into the DevTools Console:
// Access the xterm instance and read the buffer directly
const terminal = document.querySelector('.xterm').__xtermInstance;
const buffer = terminal.buffer.active;

// Read the line where the cursor was (most recent output)
const cursorLine = buffer.viewportY + terminal.rows - 2;
const line = buffer.getLine(cursorLine);
const text = line.translateToString(true);

console.log('Buffer content:', text);
console.log('Codepoints:', [...text].map(c => c.codePointAt(0).toString(16)));

What does this code do?

The terminal.buffer.active property gives you access to xterm's internal buffer. This is the data structure that stores every character the terminal has received from the PTY process.

The getLine() method fetches a specific line, and translateToString(true) converts that line's cell data into a JavaScript string. By printing the Unicode codepoints, you can verify the exact characters stored in memory.

  • Examine the Console output.

You should see the Arabic text in its correct order with the correct codepoints (Arabic block starts at U+0600). The buffer stores the text correctly.

What does this prove?

If the buffer contains the correct Arabic codepoints in the correct order, it means the shell, the PTY transport layer, and the xterm data parser are all working perfectly. The bug is NOT in the data path. It must be in the rendering path that takes this buffer data and paints it on screen.

This is a critical diagnostic boundary. You have now ruled out three potential failure points in a single check.

Not seeing Arabic codepoints in the output?

Make sure you echoed the Arabic text into the terminal pane before running the diagnostic snippet. The cursorLine calculation reads near the bottom of the viewport, so the echo output must be visible.

If __xtermInstance is undefined, try accessing the terminal via Orca's internal references. Check existing e2e tests in tests/e2e/ for the exact access pattern used in this codebase.

Still stuck? Help me access the xterm buffer in Orca's DevTools console.

Isolate the rendering layer

You know the buffer is correct. Now you need to confirm that xterm's renderer is the broken layer. The key insight is that Arabic script has two properties that a renderer must support:

  • Right-to-left ordering (words flow from right to left).
  • Contextual letter shaping (Arabic letters connect differently depending on their position in a word: initial, medial, final, or isolated).

The xterm.js renderer paints text cell-by-cell into a canvas using a WebGL or DOM renderer. Each cell is an independent unit with no awareness of its neighbors. This cell-by-cell model breaks both Arabic properties.

  • Confirm this by testing the same text in the browser's native text engine. Paste this into the DevTools Console:
// Create a native HTML paragraph with the same Arabic text
const testDiv = document.createElement('div');
testDiv.style.cssText = 'position:fixed; top:10px; right:10px; background:#222; color:#fff; padding:16px; font-family:monospace; font-size:14px; z-index:99999; direction:rtl;';
testDiv.textContent = 'مرحباً هذه مشكلة في اللغة العربية';
document.body.appendChild(testDiv);
  • Compare the floating test div (top-right corner) with the terminal output below it.

The native HTML paragraph renders the Arabic text correctly: connected letters, right-to-left flow. The terminal renders the same text with disconnected letters in left-to-right order. This proves the browser's native text engine handles Arabic shaping correctly, but xterm's custom renderer does not.

Why can't xterm.js just fix this?

This is a known upstream limitation tracked as xterm.js issue #701, open since 2017. The cell-by-cell rendering model is fundamental to how xterm.js works. It enables features like precise cursor positioning, selection, and character-grid alignment.

Fixing it inside xterm.js would require rearchitecting the renderer to handle variable-width, context-dependent character shaping. That is a massive change that has not been completed in over eight years of discussion.

  • Clean up the test div when you are done by pasting:
// Remove the test div
document.querySelector('div[style*="z-index:99999"]').remove();

Both the div and terminal look broken?

If the floating div also shows disconnected Arabic text, your system may be missing Arabic font support. Install a font that includes Arabic glyphs (such as Noto Sans Arabic or DejaVu Sans) and restart Orca.

Need help? Help me install Arabic font support on my system.

Determine the fix strategy

You have now proven: the buffer is correct, and the browser's native text engine renders Arabic perfectly. The only broken layer is xterm.js's custom renderer.

You cannot modify xterm.js's internal rendering without forking the entire library. But you do not need to. The contributor who filed PR #6793 on the Orca repository used a pragmatic approach: render a DOM overlay on top of xterm for rows containing Arabic text, and let the browser's native text engine do the shaping work.

Why a DOM overlay instead of patching xterm.js?

Forking @xterm/xterm creates a maintenance burden. Every upstream update would require rebasing your changes. The overlay approach is surgical: it only activates for rows that contain RTL characters, leaving everything else untouched.

The browser already has a world-class text shaping engine built in. By rendering affected rows in a positioned DOM element, you delegate the hard work (contextual shaping, bidirectional reordering) to the browser. This is the same technique used by the ar-terminal VS Code extension.

The overlay must satisfy three requirements to work correctly:

  • Detect Arabic and Hebrew characters using Unicode ranges so it only activates when needed.
  • Match xterm's font metrics (family, size, line height) so the overlay text aligns perfectly with the terminal grid.
  • Clean up on pane disposal so there are no memory leaks or orphaned DOM elements.

How will this overlay integrate with Orca?

The overlay will be a new module at src/renderer/src/lib/pane-manager/terminal-rtl-overlay.ts. It will attach to the terminal pane lifecycle: created when a terminal opens, and its cleanup function called when the pane is disposed. This keeps the fix scoped to the terminal rendering path without touching PTY logic, selection, or SSH behavior.

You have systematically narrowed the problem from "Arabic text looks wrong" to a precise diagnosis: xterm.js renders cell-by-cell, which breaks Arabic contextual shaping and RTL ordering. Next up, you will implement the DOM overlay fix that leverages the browser's native text engine.

Implement the Fix

You have a diagnosis that proves the rendering layer is the problem. The xterm.js cell-by-cell model strips Arabic letters of their connections and reverses their order.

In this step, you will build a DOM overlay that renders affected terminal rows using the browser's native text engine. The browser already knows how to shape Arabic letters and lay them out right-to-left. Your job is to detect which rows need help, position a transparent layer on top of xterm's canvas, and let the browser do the heavy lifting.

In this step, get ready to:
  • Create a TypeScript module that detects RTL characters and renders a positioned overlay.
  • Add CSS styles and wire the overlay into the terminal pane lifecycle.
  • Verify the fix renders Arabic correctly and all tests pass.
Create the Arabic text detection and overlay module

The overlay approach works by detecting rows that contain RTL characters, reading their text from the xterm buffer, and rendering that text in a positioned div that sits on top of the terminal canvas. The browser's native text engine handles shaping and direction automatically.

  • Create the file src/renderer/src/lib/pane-manager/terminal-rtl-overlay.ts and add the detection utilities and buffer helper by pasting this code:
import type { Terminal } from '@xterm/xterm'

// Unicode ranges covering Arabic (multiple blocks) and Hebrew scripts
const RTL_REGEX =
  /[\u0590-\u05FF\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\uFB50-\uFDFF\uFE70-\uFEFF]/

// Returns true if the string contains any RTL characters needing overlay rendering
export function containsRtlText(text: string): boolean {
  return RTL_REGEX.test(text)
}

// Reads the text content of a single visible row from xterm's active buffer
function getVisibleRowText(terminal: Terminal, rowIndex: number): string {
  const buffer = terminal.buffer.active
  const line = buffer.getLine(buffer.viewportY + rowIndex)
  if (!line) return ''
  return line.translateToString(true)
}

What does this code do?

RTL_REGEX matches characters from Arabic (including presentation forms and extended blocks) and Hebrew Unicode ranges. Any single match means the row needs overlay rendering.

containsRtlText() is the public detection function other modules can import. getVisibleRowText() reads a row from xterm's internal buffer using translateToString(true) which trims trailing whitespace.

  • Below the buffer helper, add the font metrics extraction function:
// Reads xterm's computed font metrics so the overlay aligns precisely with terminal cells
function getTerminalFontMetrics(xtermContainer: HTMLElement): {
  fontFamily: string
  fontSize: string
  lineHeight: string
  cellWidth: number
  cellHeight: number
} {
  const xtermScreen = xtermContainer.querySelector('.xterm-screen')
  const xtermRows = xtermContainer.querySelector('.xterm-rows')
  if (!xtermScreen || !xtermRows) {
    return {
      fontFamily: 'monospace',
      fontSize: '14px',
      lineHeight: '1.2',
      cellWidth: 8,
      cellHeight: 17,
    }
  }

  const computedStyle = getComputedStyle(xtermRows)
  const cellWidthAttr = xtermContainer.querySelector('.xterm-char-measure-element')
  const rows = xtermContainer.querySelectorAll('.xterm-rows > div')
  const cellHeight = rows.length > 1 ? rows[1].getBoundingClientRect().top - rows[0].getBoundingClientRect().top : 17

  return {
    fontFamily: computedStyle.fontFamily || 'monospace',
    fontSize: computedStyle.fontSize || '14px',
    lineHeight: `${cellHeight}px`,
    cellWidth: cellWidthAttr?.getBoundingClientRect().width || 8,
    cellHeight,
  }
}

Why match xterm's font metrics?

The overlay must sit perfectly on top of xterm's rendered rows. If the font family, size, or line height drifts even slightly, the Arabic text will misalign with the cursor and surrounding characters.

The function reads computed styles directly from xterm's DOM elements. The fallback values (monospace, 14px, 17px cell height) cover edge cases where the DOM hasn't fully rendered yet.

  • Below the metrics function, add the core rendering logic that builds overlay rows:
// Scans visible terminal rows and creates overlay divs for any row containing RTL text
function renderOverlayContent(
  terminal: Terminal,
  overlay: HTMLElement,
  xtermContainer: HTMLElement
): void {
  const metrics = getTerminalFontMetrics(xtermContainer)
  const viewportRows = terminal.rows

  // Clear previous overlay content before re-rendering
  overlay.innerHTML = ''

  for (let row = 0; row < viewportRows; row++) {
    const text = getVisibleRowText(terminal, row)

    // Skip rows without RTL characters to avoid performance overhead
    if (!containsRtlText(text)) continue

    // Create a positioned div that overlays this specific terminal row
    const rowDiv = document.createElement('div')
    rowDiv.className = 'rtl-overlay-row'
    rowDiv.style.position = 'absolute'
    rowDiv.style.top = `${row * metrics.cellHeight}px`
    rowDiv.style.left = '0'
    rowDiv.style.right = '0'
    rowDiv.style.height = `${metrics.cellHeight}px`
    rowDiv.style.fontFamily = metrics.fontFamily
    rowDiv.style.fontSize = metrics.fontSize
    rowDiv.style.lineHeight = metrics.lineHeight
    rowDiv.style.direction = 'rtl'
    rowDiv.style.unicodeBidi = 'plaintext'
    rowDiv.style.whiteSpace = 'pre'
    rowDiv.style.pointerEvents = 'none'
    rowDiv.textContent = text

    overlay.appendChild(rowDiv)
  }
}

What does this code do?

renderOverlayContent() loops through every visible row in the terminal viewport. For each row containing RTL characters, it creates a div positioned at the exact pixel offset of that row.

The two critical CSS properties are direction: rtl (tells the browser to lay out text right-to-left) and unicode-bidi: plaintext (lets the browser apply the Unicode Bidirectional Algorithm automatically). Together they produce correct letter shaping and word order.

  • Finally, add the main entry point function that creates the overlay, wires up event listeners, and returns a cleanup function:
// Main entry point: attaches the RTL overlay and returns a cleanup function for disposal
export function attachTerminalRtlOverlay(
  terminal: Terminal,
  xtermContainer: HTMLElement
): () => void {
  // Create the overlay container positioned over the terminal
  const overlay = document.createElement('div')
  overlay.className = 'orca-terminal-rtl-overlay'
  xtermContainer.style.position = 'relative'
  xtermContainer.appendChild(overlay)

  let rafId: number | null = null

  // Debounce renders to one per animation frame for performance
  function scheduleRender(): void {
    if (rafId !== null) cancelAnimationFrame(rafId)
    rafId = requestAnimationFrame(() => {
      rafId = null
      renderOverlayContent(terminal, overlay, xtermContainer)
    })
  }

  // Subscribe to all terminal events that could change visible content
  const renderDisposable = terminal.onRender(scheduleRender)
  const writeDisposable = terminal.onWriteParsed(scheduleRender)
  const scrollDisposable = terminal.onScroll(scheduleRender)
  const resizeDisposable = terminal.onResize(scheduleRender)

  // Hide overlay during text selection so user can select from xterm's buffer
  const selectionDisposable = terminal.onSelectionChange(() => {
    const hasSelection = terminal.getSelection().length > 0
    overlay.style.display = hasSelection ? 'none' : ''
  })

  // Re-render when the container resizes (e.g. split pane drag)
  let resizeObserver: ResizeObserver | null = null
  if (typeof ResizeObserver !== 'undefined') {
    resizeObserver = new ResizeObserver(scheduleRender)
    resizeObserver.observe(xtermContainer)
  }

  // Trigger initial render
  scheduleRender()

  // Return cleanup function that tears down everything
  return () => {
    if (rafId !== null) cancelAnimationFrame(rafId)
    renderDisposable.dispose()
    writeDisposable.dispose()
    scrollDisposable.dispose()
    resizeDisposable.dispose()
    selectionDisposable.dispose()
    resizeObserver?.disconnect()
    overlay.remove()
  }
}

What does this code do?

attachTerminalRtlOverlay() is the function you will call from the pane lifecycle. It creates the overlay div, subscribes to five terminal events (render, write, scroll, resize, selection), and observes container size changes.

Every event triggers scheduleRender() which uses requestAnimationFrame to batch updates. This prevents the overlay from re-rendering dozens of times per second during fast output.

The returned cleanup function disposes every listener, cancels any pending frame, disconnects the ResizeObserver, and removes the overlay from the DOM. You will store this function and call it when the terminal pane closes.

  • Save terminal-rtl-overlay.ts.

You won't see a visible change yet. The overlay needs CSS styles and lifecycle wiring before it activates.

Seeing a TypeScript error on save?

If your editor shows a red squiggle on the import type { Terminal } from '@xterm/xterm' line, confirm you ran pnpm install in Step 1. The @xterm/xterm package must be in node_modules for TypeScript to resolve the type.

help me fix a TypeScript import error for @xterm/xterm in my overlay module

✔️ Awesome, I've got everything!

Great. Double check you have saved terminal-rtl-overlay.ts before moving on.

ⓧ I'd like to double check the full code

import type { Terminal } from '@xterm/xterm'

const RTL_REGEX =
  /[\u0590-\u05FF\u0600-\u06FF\u0750-\u077F\u08A0-\u08FF\uFB50-\uFDFF\uFE70-\uFEFF]/

export function containsRtlText(text: string): boolean {
  return RTL_REGEX.test(text)
}

function getVisibleRowText(terminal: Terminal, rowIndex: number): string {
  const buffer = terminal.buffer.active
  const line = buffer.getLine(buffer.viewportY + rowIndex)
  if (!line) return ''
  return line.translateToString(true)
}

function getTerminalFontMetrics(xtermContainer: HTMLElement): {
  fontFamily: string
  fontSize: string
  lineHeight: string
  cellWidth: number
  cellHeight: number
} {
  const xtermScreen = xtermContainer.querySelector('.xterm-screen')
  const xtermRows = xtermContainer.querySelector('.xterm-rows')
  if (!xtermScreen || !xtermRows) {
    return {
      fontFamily: 'monospace',
      fontSize: '14px',
      lineHeight: '1.2',
      cellWidth: 8,
      cellHeight: 17,
    }
  }

  const computedStyle = getComputedStyle(xtermRows)
  const cellWidthAttr = xtermContainer.querySelector('.xterm-char-measure-element')
  const rows = xtermContainer.querySelectorAll('.xterm-rows > div')
  const cellHeight = rows.length > 1 ? rows[1].getBoundingClientRect().top - rows[0].getBoundingClientRect().top : 17

  return {
    fontFamily: computedStyle.fontFamily || 'monospace',
    fontSize: computedStyle.fontSize || '14px',
    lineHeight: `${cellHeight}px`,
    cellWidth: cellWidthAttr?.getBoundingClientRect().width || 8,
    cellHeight,
  }
}

function renderOverlayContent(
  terminal: Terminal,
  overlay: HTMLElement,
  xtermContainer: HTMLElement
): void {
  const metrics = getTerminalFontMetrics(xtermContainer)
  const viewportRows = terminal.rows

  overlay.innerHTML = ''

  for (let row = 0; row < viewportRows; row++) {
    const text = getVisibleRowText(terminal, row)

    if (!containsRtlText(text)) continue

    const rowDiv = document.createElement('div')
    rowDiv.className = 'rtl-overlay-row'
    rowDiv.style.position = 'absolute'
    rowDiv.style.top = `${row * metrics.cellHeight}px`
    rowDiv.style.left = '0'
    rowDiv.style.right = '0'
    rowDiv.style.height = `${metrics.cellHeight}px`
    rowDiv.style.fontFamily = metrics.fontFamily
    rowDiv.style.fontSize = metrics.fontSize
    rowDiv.style.lineHeight = metrics.lineHeight
    rowDiv.style.direction = 'rtl'
    rowDiv.style.unicodeBidi = 'plaintext'
    rowDiv.style.whiteSpace = 'pre'
    rowDiv.style.pointerEvents = 'none'
    rowDiv.textContent = text

    overlay.appendChild(rowDiv)
  }
}

export function attachTerminalRtlOverlay(
  terminal: Terminal,
  xtermContainer: HTMLElement
): () => void {
  const overlay = document.createElement('div')
  overlay.className = 'orca-terminal-rtl-overlay'
  xtermContainer.style.position = 'relative'
  xtermContainer.appendChild(overlay)

  let rafId: number | null = null

  function scheduleRender(): void {
    if (rafId !== null) cancelAnimationFrame(rafId)
    rafId = requestAnimationFrame(() => {
      rafId = null
      renderOverlayContent(terminal, overlay, xtermContainer)
    })
  }

  const renderDisposable = terminal.onRender(scheduleRender)
  const writeDisposable = terminal.onWriteParsed(scheduleRender)
  const scrollDisposable = terminal.onScroll(scheduleRender)
  const resizeDisposable = terminal.onResize(scheduleRender)

  const selectionDisposable = terminal.onSelectionChange(() => {
    const hasSelection = terminal.getSelection().length > 0
    overlay.style.display = hasSelection ? 'none' : ''
  })

  let resizeObserver: ResizeObserver | null = null
  if (typeof ResizeObserver !== 'undefined') {
    resizeObserver = new ResizeObserver(scheduleRender)
    resizeObserver.observe(xtermContainer)
  }

  scheduleRender()

  return () => {
    if (rafId !== null) cancelAnimationFrame(rafId)
    renderDisposable.dispose()
    writeDisposable.dispose()
    scrollDisposable.dispose()
    resizeDisposable.dispose()
    selectionDisposable.dispose()
    resizeObserver?.disconnect()
    overlay.remove()
  }
}
Add CSS and integrate into the pane lifecycle

The overlay module creates DOM elements, but without CSS they won't layer correctly over xterm's canvas. You also need to call attachTerminalRtlOverlay() at the right point in the terminal pane lifecycle.

  • Open src/renderer/src/assets/terminal.css and add these styles at the end of the file:
/* RTL overlay for Arabic/Hebrew text rendering */
.orca-terminal-rtl-overlay {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  pointer-events: none;
  z-index: 1;
  overflow: hidden;
}

.orca-terminal-rtl-overlay .rtl-overlay-row {
  position: absolute;
  left: 0;
  right: 0;
  pointer-events: none;
  overflow: hidden;
  text-overflow: clip;
  background: inherit;
}

What do these styles do?

The .orca-terminal-rtl-overlay container stretches to fill the terminal pane and sits above the xterm canvas (z-index: 1). The pointer-events: none rule ensures clicks pass through to xterm so the user can still interact with the terminal normally.

Each .rtl-overlay-row is absolutely positioned at the pixel offset of its corresponding terminal row. The background: inherit rule lets the row pick up the terminal's background color so it visually covers xterm's incorrectly rendered text.

  • Save terminal.css.
  • Open src/renderer/src/lib/pane-manager/pane-lifecycle.ts.
  • At the top of the file, import the overlay function:
import { attachTerminalRtlOverlay } from './terminal-rtl-overlay'
  • Find the section where a new terminal pane is created and the Terminal instance becomes available. After the terminal is attached to the DOM, call attachTerminalRtlOverlay(terminal, xtermContainer) and store the returned cleanup function.
  • Find the pane disposal logic (where listeners and resources are cleaned up). Call the stored cleanup function there so the overlay is removed when the pane closes.
  • Save pane-lifecycle.ts.

Why wire cleanup into disposal?

Orca creates and destroys terminal panes frequently (splits, closes, tab switches). Without cleanup, every closed pane would leave orphaned DOM nodes, event listeners, and ResizeObserver instances behind. Over time this causes memory leaks and phantom re-renders.

  • Return to your running dev instance (or restart it with pnpm dev) and open a terminal pane.
  • Type or run the same echo command from earlier:
echo "مرحباً هذه مشكلة"
  • Confirm the Arabic text now renders with connected letters flowing right-to-left.

Compare this to what you saw during reproduction. The letters should be joined (not disconnected) and the word order should be natural Arabic reading order.

Arabic text still looks disconnected?

Check that your import path in pane-lifecycle.ts matches the file you created: './terminal-rtl-overlay'. A wrong path means the overlay never attaches.

Open DevTools (Ctrl+Shift+I) and look for a .orca-terminal-rtl-overlay element in the DOM. If it does not exist, the attachTerminalRtlOverlay() call is not being reached. Check the console for errors.

help me debug why my RTL overlay is not appearing in the terminal

Verify the fix with automated tests

Your manual check confirmed the fix works visually. Now run the automated tests to prove it programmatically and ensure nothing else broke.

  • Run your e2e test to confirm it now passes:
pnpm run test:e2e -- tests/e2e/terminal-arabic-rtl-rendering.spec.ts

What should I see?

All three test cases should pass: Arabic text renders in RTL order, English text does not trigger the overlay, and the overlay updates on scroll. The test that previously FAILED in Step 2 should now be green.

  • Run the full test suite to confirm no regressions:
pnpm test

All existing unit tests should pass alongside your new overlay behavior. A clean run here means your fix does not break any other Orca functionality.

E2e test still failing?

If the test cannot find .orca-terminal-rtl-overlay, the overlay is not mounting before the test assertion runs. Check that scheduleRender() is called immediately in attachTerminalRtlOverlay() (not only on events). The initial render must fire on attach.

If pnpm test shows failures in unrelated tests, run pnpm typecheck first. A type error in your new file can cascade to other compilation units.

help me fix my failing e2e test for the RTL overlay

Your fix is verified. Arabic text renders correctly, automated tests prove it, and the full suite passes clean. Next up, you will write dedicated regression tests to lock this fix in permanently.

Write Regression Tests

Your fix works in the dev build and the e2e test passes. But a fix without regression tests is a ticking time bomb.

In this step, you will write a Vitest unit test suite that exercises the overlay logic in isolation. Then you will run the full CI check suite to confirm nothing is broken.

In this step, get ready to:
  • Create unit tests for the RTL detection and overlay attachment functions.
  • Confirm your e2e tests cover the key rendering scenarios.
  • Run lint, typecheck, and test commands to pass CI.
Create unit tests for the overlay logic

Unit tests exercise individual functions in isolation without launching the full Electron app. They run in milliseconds, so they catch regressions instantly during development.

  • Create a new file at src/renderer/src/lib/pane-manager/terminal-rtl-overlay.test.ts.
  • Add the imports and the containsRtlText test suite by pasting this code:
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { containsRtlText, attachTerminalRtlOverlay } from './terminal-rtl-overlay'

describe('containsRtlText', () => {
  it('returns true for Arabic text', () => {
    expect(containsRtlText('مرحباً')).toBe(true)
  })

  it('returns true for Hebrew text', () => {
    expect(containsRtlText('שלום')).toBe(true)
  })

  it('returns false for English text', () => {
    expect(containsRtlText('hello world')).toBe(false)
  })

  it('returns false for empty string', () => {
    expect(containsRtlText('')).toBe(false)
  })

  it('returns true for mixed Arabic and English', () => {
    expect(containsRtlText('Error in ملف')).toBe(true)
  })

  it('returns false for numbers and punctuation only', () => {
    expect(containsRtlText('123 !@# $%^')).toBe(false)
  })

  it('returns true for Arabic presentation forms', () => {
    // U+FE70 range: Arabic Presentation Forms-B
    expect(containsRtlText('\uFE70')).toBe(true)
  })
})

What does this code do?

This test suite verifies that containsRtlText() correctly identifies strings containing RTL characters.

  • The first two tests confirm detection of Arabic and Hebrew scripts (both are RTL).
  • The English, empty, and numbers-only tests confirm the function does not produce false positives for LTR content.
  • The mixed and presentation-forms tests cover edge cases. Arabic Presentation Forms (U+FE70) are pre-shaped glyphs that must still trigger the overlay.

Now you need a helper that simulates xterm.js terminal events, plus the full attachTerminalRtlOverlay integration test suite.

  • Add the following code below the closing }) of the containsRtlText describe block:
// Helper to create a mock terminal event emitter
function createTerminalEvent() {
  const listeners: Array<(...args: any[]) => void> = []
  return {
    event: (listener: (...args: any[]) => void) => {
      listeners.push(listener)
      return { dispose: () => listeners.splice(listeners.indexOf(listener), 1) }
    },
    fire: (...args: any[]) => listeners.forEach((l) => l(...args)),
    listenerCount: () => listeners.length,
  }
}

describe('attachTerminalRtlOverlay', () => {
  let terminal: any
  let xtermContainer: HTMLElement
  let renderEvent: ReturnType<typeof createTerminalEvent>
  let writeEvent: ReturnType<typeof createTerminalEvent>
  let scrollEvent: ReturnType<typeof createTerminalEvent>
  let resizeEvent: ReturnType<typeof createTerminalEvent>
  let selectionEvent: ReturnType<typeof createTerminalEvent>

  beforeEach(() => {
    renderEvent = createTerminalEvent()
    writeEvent = createTerminalEvent()
    scrollEvent = createTerminalEvent()
    resizeEvent = createTerminalEvent()
    selectionEvent = createTerminalEvent()

    terminal = {
      rows: 24,
      buffer: {
        active: {
          viewportY: 0,
          getLine: vi.fn((row: number) => ({
            translateToString: vi.fn(() => {
              if (row === 0) return 'مرحباً هذه مشكلة'
              if (row === 1) return 'hello world'
              return ''
            }),
          })),
        },
      },
      onRender: renderEvent.event,
      onWriteParsed: writeEvent.event,
      onScroll: scrollEvent.event,
      onResize: resizeEvent.event,
      onSelectionChange: selectionEvent.event,
      getSelection: vi.fn(() => ''),
    }

    xtermContainer = document.createElement('div')
    xtermContainer.innerHTML = `
      <div class="xterm-screen">
        <div class="xterm-rows">
          <div></div>
          <div></div>
        </div>
      </div>
    `
    document.body.appendChild(xtermContainer)

    vi.stubGlobal('requestAnimationFrame', vi.fn((cb: () => void) => {
      cb()
      return 1
    }))
    vi.stubGlobal('cancelAnimationFrame', vi.fn())
  })

  it('creates an overlay element inside the container', () => {
    const cleanup = attachTerminalRtlOverlay(terminal, xtermContainer)
    const overlay = xtermContainer.querySelector('.orca-terminal-rtl-overlay')
    expect(overlay).not.toBeNull()
    cleanup()
  })

  it('renders RTL rows for lines containing Arabic text', () => {
    const cleanup = attachTerminalRtlOverlay(terminal, xtermContainer)
    const rows = xtermContainer.querySelectorAll('.rtl-overlay-row')
    // Row 0 has Arabic, row 1 is English-only
    expect(rows.length).toBe(1)
    expect(rows[0].textContent).toBe('مرحباً هذه مشكلة')
    expect((rows[0] as HTMLElement).style.direction).toBe('rtl')
    cleanup()
  })

  it('does not render overlay rows for English-only lines', () => {
    terminal.buffer.active.getLine = vi.fn(() => ({
      translateToString: vi.fn(() => 'hello world'),
    }))

    const cleanup = attachTerminalRtlOverlay(terminal, xtermContainer)
    const rows = xtermContainer.querySelectorAll('.rtl-overlay-row')
    expect(rows.length).toBe(0)
    cleanup()
  })

  it('hides overlay during text selection', () => {
    const cleanup = attachTerminalRtlOverlay(terminal, xtermContainer)
    const overlay = xtermContainer.querySelector('.orca-terminal-rtl-overlay') as HTMLElement

    terminal.getSelection.mockReturnValue('selected text')
    selectionEvent.fire()

    expect(overlay.style.display).toBe('none')

    terminal.getSelection.mockReturnValue('')
    selectionEvent.fire()

    expect(overlay.style.display).toBe('')
    cleanup()
  })

  it('disposes listeners, pending render work, and overlay DOM', () => {
    const cleanup = attachTerminalRtlOverlay(terminal, xtermContainer)
    expect(xtermContainer.querySelector('.orca-terminal-rtl-overlay')).not.toBeNull()
    expect(renderEvent.listenerCount()).toBe(1)

    cleanup()

    expect(xtermContainer.querySelector('.orca-terminal-rtl-overlay')).toBeNull()
    expect(renderEvent.listenerCount()).toBe(0)
    expect(scrollEvent.listenerCount()).toBe(0)
  })
})

What does this code do?

The createTerminalEvent() helper mimics the xterm.js event system. Each event object has event() to register listeners, fire() to trigger them, and listenerCount() to verify cleanup.

The beforeEach block builds a fake terminal with two rows (one Arabic, one English) and a minimal DOM structure. It also stubs requestAnimationFrame to run synchronously so tests don't need timers.

  • The overlay creation test confirms the DOM element is injected.
  • The RTL rows test proves only Arabic lines get overlay rows (row 0 yes, row 1 no).
  • The selection test verifies the overlay hides during selection so users can still copy from the xterm buffer.
  • The disposal test confirms all listeners are removed and the overlay DOM is cleaned up, preventing memory leaks.
  • Save terminal-rtl-overlay.test.ts.

Seeing import errors in your editor?

Make sure the import path matches the source file exactly: ./terminal-rtl-overlay (no .ts extension). Also confirm that containsRtlText and attachTerminalRtlOverlay are both exported from the source file.

Help me fix import errors in my Vitest test file.

✔️ Awesome, I've got everything!

Great. Double check you have saved the file before moving on.

ⓧ I'd like to double check the full code

import { describe, it, expect, vi, beforeEach } from 'vitest'
import { containsRtlText, attachTerminalRtlOverlay } from './terminal-rtl-overlay'

describe('containsRtlText', () => {
  it('returns true for Arabic text', () => {
    expect(containsRtlText('مرحباً')).toBe(true)
  })

  it('returns true for Hebrew text', () => {
    expect(containsRtlText('שלום')).toBe(true)
  })

  it('returns false for English text', () => {
    expect(containsRtlText('hello world')).toBe(false)
  })

  it('returns false for empty string', () => {
    expect(containsRtlText('')).toBe(false)
  })

  it('returns true for mixed Arabic and English', () => {
    expect(containsRtlText('Error in ملف')).toBe(true)
  })

  it('returns false for numbers and punctuation only', () => {
    expect(containsRtlText('123 !@# $%^')).toBe(false)
  })

  it('returns true for Arabic presentation forms', () => {
    // U+FE70 range: Arabic Presentation Forms-B
    expect(containsRtlText('\uFE70')).toBe(true)
  })
})

// Helper to create a mock terminal event emitter
function createTerminalEvent() {
  const listeners: Array<(...args: any[]) => void> = []
  return {
    event: (listener: (...args: any[]) => void) => {
      listeners.push(listener)
      return { dispose: () => listeners.splice(listeners.indexOf(listener), 1) }
    },
    fire: (...args: any[]) => listeners.forEach((l) => l(...args)),
    listenerCount: () => listeners.length,
  }
}

describe('attachTerminalRtlOverlay', () => {
  let terminal: any
  let xtermContainer: HTMLElement
  let renderEvent: ReturnType<typeof createTerminalEvent>
  let writeEvent: ReturnType<typeof createTerminalEvent>
  let scrollEvent: ReturnType<typeof createTerminalEvent>
  let resizeEvent: ReturnType<typeof createTerminalEvent>
  let selectionEvent: ReturnType<typeof createTerminalEvent>

  beforeEach(() => {
    renderEvent = createTerminalEvent()
    writeEvent = createTerminalEvent()
    scrollEvent = createTerminalEvent()
    resizeEvent = createTerminalEvent()
    selectionEvent = createTerminalEvent()

    terminal = {
      rows: 24,
      buffer: {
        active: {
          viewportY: 0,
          getLine: vi.fn((row: number) => ({
            translateToString: vi.fn(() => {
              if (row === 0) return 'مرحباً هذه مشكلة'
              if (row === 1) return 'hello world'
              return ''
            }),
          })),
        },
      },
      onRender: renderEvent.event,
      onWriteParsed: writeEvent.event,
      onScroll: scrollEvent.event,
      onResize: resizeEvent.event,
      onSelectionChange: selectionEvent.event,
      getSelection: vi.fn(() => ''),
    }

    xtermContainer = document.createElement('div')
    xtermContainer.innerHTML = `
      <div class="xterm-screen">
        <div class="xterm-rows">
          <div></div>
          <div></div>
        </div>
      </div>
    `
    document.body.appendChild(xtermContainer)

    vi.stubGlobal('requestAnimationFrame', vi.fn((cb: () => void) => {
      cb()
      return 1
    }))
    vi.stubGlobal('cancelAnimationFrame', vi.fn())
  })

  it('creates an overlay element inside the container', () => {
    const cleanup = attachTerminalRtlOverlay(terminal, xtermContainer)
    const overlay = xtermContainer.querySelector('.orca-terminal-rtl-overlay')
    expect(overlay).not.toBeNull()
    cleanup()
  })

  it('renders RTL rows for lines containing Arabic text', () => {
    const cleanup = attachTerminalRtlOverlay(terminal, xtermContainer)
    const rows = xtermContainer.querySelectorAll('.rtl-overlay-row')
    // Row 0 has Arabic, row 1 is English-only
    expect(rows.length).toBe(1)
    expect(rows[0].textContent).toBe('مرحباً هذه مشكلة')
    expect((rows[0] as HTMLElement).style.direction).toBe('rtl')
    cleanup()
  })

  it('does not render overlay rows for English-only lines', () => {
    terminal.buffer.active.getLine = vi.fn(() => ({
      translateToString: vi.fn(() => 'hello world'),
    }))

    const cleanup = attachTerminalRtlOverlay(terminal, xtermContainer)
    const rows = xtermContainer.querySelectorAll('.rtl-overlay-row')
    expect(rows.length).toBe(0)
    cleanup()
  })

  it('hides overlay during text selection', () => {
    const cleanup = attachTerminalRtlOverlay(terminal, xtermContainer)
    const overlay = xtermContainer.querySelector('.orca-terminal-rtl-overlay') as HTMLElement

    terminal.getSelection.mockReturnValue('selected text')
    selectionEvent.fire()

    expect(overlay.style.display).toBe('none')

    terminal.getSelection.mockReturnValue('')
    selectionEvent.fire()

    expect(overlay.style.display).toBe('')
    cleanup()
  })

  it('disposes listeners, pending render work, and overlay DOM', () => {
    const cleanup = attachTerminalRtlOverlay(terminal, xtermContainer)
    expect(xtermContainer.querySelector('.orca-terminal-rtl-overlay')).not.toBeNull()
    expect(renderEvent.listenerCount()).toBe(1)

    cleanup()

    expect(xtermContainer.querySelector('.orca-terminal-rtl-overlay')).toBeNull()
    expect(renderEvent.listenerCount()).toBe(0)
    expect(scrollEvent.listenerCount()).toBe(0)
  })
})
Confirm e2e test coverage

Your e2e test file from Step 2 already covers the three critical rendering scenarios: pure Arabic RTL rendering, English-only text not triggering the overlay, and overlay updates on scroll.

  • Open tests/e2e/terminal-arabic-rtl-rendering.spec.ts and confirm it contains these three test cases:
  • Arabic text renders with connected letters in RTL order verifies the overlay appears and has direction: rtl.
  • English-only text does not trigger RTL overlay confirms no performance overhead for Latin text.
  • overlay updates on terminal scroll checks the overlay stays visible after scrolling.

Why both e2e and unit tests?

The e2e tests exercise the full rendering pipeline (Electron main process, renderer, xterm.js, DOM). They catch integration-level regressions like the overlay failing to attach during the pane lifecycle.

The unit tests exercise the detection logic and DOM manipulation in isolation. They run in under a second and catch logic errors like a broken regex or a missing cleanup call. Both layers together form a strong safety net.

Run CI checks to confirm everything passes

Before any open-source contribution can be merged, the CI pipeline must pass. Orca's CI runs three checks: lint, typecheck, and tests.

  • Confirm your code style passes by running:
pnpm lint
  • You should see no errors in the output. If the linter flags formatting issues in your test file, fix them and re-run.
  • Confirm TypeScript compiles cleanly by running:
pnpm typecheck
  • You should see a clean compilation with no type errors.
  • Run the full test suite (including your new unit tests) by running:
pnpm test
  • You should see all tests pass, including your new containsRtlText and attachTerminalRtlOverlay suites.

Tests failing?

If your unit tests fail with a containsRtlText is not exported error, check that terminal-rtl-overlay.ts uses export function containsRtlText (not just function without the export keyword).

If the typecheck fails, look for mismatched types in your mock terminal object. Every property the overlay function reads must exist on the mock.

Help me debug failing unit tests in my RTL overlay test suite.

You now have a complete bug-fix pipeline: a reproduction test, a root-cause diagnosis, a verified fix, and a regression test suite that would catch this bug if it ever resurfaces. This is exactly what production-grade open-source contributions look like.

Secret mission

Handle Mixed Bidirectional Text

Real terminal output rarely contains pure Arabic. Error messages mix English keywords with Arabic filenames, and numbers appear inline. Can you extend the overlay to handle mixed bidirectional text correctly using the Unicode Bidirectional Algorithm?

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally, so there are no ongoing costs. Decide whether to keep your local clone of the Orca repository or delete it to reclaim disk space.

Resources you used:

  • Local clone of the Orca repository (~/Desktop/orca).

Keep everything running

No action needed. Choose this if you plan to continue contributing to Orca or want to explore the codebase further.

  • Your local clone stays at ~/Desktop/orca and is ready for further development.
  • You can push your fix branch and open a PR against stablyai/orca following their CONTRIBUTING.md guidelines.

Pause - I'll come back to this later

There are no running processes or services to stop. The clone is just files on disk, so pausing and keeping are equivalent for this project.

  • If you had pnpm dev running, press Ctrl+C in that terminal to stop the dev server.
  • Your code and branch remain intact for when you return.

Delete - I don't want to use this again

Remove the entire Orca clone to reclaim disk space.

  • If you have pnpm dev running, press Ctrl+C to stop it first.
  • Delete the Orca repository clone by running:
cd ~/Desktop
rm -rf orca
  • Confirm the folder is gone by running:
ls ~/Desktop | grep orca

You should see no output, confirming the directory has been removed.

Nice Work!

Nice Work!

You just reproduced, diagnosed, and fixed a real P1 rendering bug in a 10K-star open-source IDE. That is the exact workflow professional engineers use to tackle production bugs in complex codebases.

You learned how to:

  • Systematically reproduce a bug by reading the issue report, manually confirming the behavior, and automating the reproduction with a Playwright e2e test that fails on the unfixed code.
  • Diagnose a root cause across rendering layers by instrumenting at the PTY buffer boundary, proving the data was correct, and isolating the break to xterm.js's cell-by-cell rendering model.
  • Implement a DOM overlay fix that leverages the browser's native BiDi text engine, then verify it with both e2e and Vitest unit tests to guard against regressions.
  • Secret Mission: Extended the overlay to handle mixed bidirectional text (Arabic + English + numbers) using the Unicode Bidirectional Algorithm.

Ready to quiz yourself?