Skip to content

feat(tui): native Arabic/RTL (bidi) support for the opencode2 beta - #48590

Open
muhamedbeshir wants to merge 7 commits into
anomalyco:betafrom
muhamedbeshir:oc2-rtl-ar-fix
Open

feat(tui): native Arabic/RTL (bidi) support for the opencode2 beta#48590
muhamedbeshir wants to merge 7 commits into
anomalyco:betafrom
muhamedbeshir:oc2-rtl-ar-fix

Conversation

@muhamedbeshir

Copy link
Copy Markdown

Issue for this PR

Refs #38524, #40004, #39525, #32984. The dev-line PR (#48587) carries the auto-closing references; this PR is the same fix for the beta line so opencode2 behaves the same way.

Type of change

  • New feature

What does this PR do?

The beta line is a different codebase from dev (packages are named @opencode/*, TUI is @opencode/tui 2.0.0, and it pins @opentui/core 0.5.10), so this is a port rather than a cherry-pick. It applies the same Unicode Bidirectional Algorithm support (UAX #9) to the interactive TUI:

  • packages/tui/src/util/bidi.ts — bidi engine over bidi-js: paragraph direction from the first strong character (P2/P3), LRI/PDI isolates around LTR islands (URLs, paths, dotted/dashed identifiers, numbers, code spans), grapheme- and width-aware wrapping, and logical<->visual boundary maps.
  • packages/tui/src/component/bidi-textarea.tsTextareaRenderable subclass for the prompt: visual caret and visual arrow motion; the edit buffer stays logical, so typing, paste, undo and values are unchanged.
  • packages/tui/src/component/bidi-markdown.ts — adds withBidiMarkdown(base), which composes with the renderNode the TUI already passes (plugins.markdown()); plugin renderables win, and everything else falls through to the bidi paragraph/heading painter. Fenced code, tables and diffs keep the stock LTR renderer.
  • packages/tui/src/component/bidi-text.tsTextRenderable subclass for user message echoes.
  • Wiring: <textarea> -> <bidi_textarea> in component/prompt/index.tsx; renderNode={withBidiMarkdown(plugins.markdown())} in routes/session/message-parts.tsx and routes/session/index.tsx; user message text -> <bidi_text>.

English-only content takes the stock paint path unchanged, and selection/copy returns the original logical text (isolates are layout-only and are never written to a buffer).

How did you verify your code works?

On Windows 11, on this branch:

bun install --frozen-lockfile
bun run --cwd packages/tui typecheck        # exit 0
bun test --cwd packages/tui ./test/util/bidi.test.ts ./test/component/bidi-render.test.tsx

Result: 31 pass, 0 fail (220 assertions). The component tests use the real OpenTUI 0.5.10 test renderer and assert the painted cell frame: RTL order, right alignment, fenced code LTR, English-only output identical to the stock renderer, caret geometry, and selection copy returning logical text.

Screenshots / recordings

Text-only change. Two of the exact cell-order frames the tests assert (a real terminal renders them right-to-left because the cells are already in visual order):

Arabic on its own, right-aligned:

                                 ملاعلاب ابحرم

Mixed Arabic + English, English run intact:

                     ةمزحلا تيبثتل npm install مدختسا

Checklist

  • I have tested my changes locally
  • I have not included unrelated changes in this PR

Port of the UAX anomalyco#9 fix to the opencode2 (v2) codebase, which uses @opentui/core 0.5.10.

- bidi engine (bidi-js wrapper): first-strong paragraph direction, LRI/PDI isolation for URLs/paths/numbers/code spans, grapheme/width-aware wrapping, logical<->visual caret maps.
- BidiTextareaRenderable for the prompt; edit buffer stays logical.
- withBidiMarkdown composes with the existing plugins.markdown() renderNode for assistant paragraphs/headings; code/tables/diffs keep the stock LTR renderer.
- BidiTextRenderable for user message echoes.
- English-only content takes the stock render path; selection/copy stays logical.
- Tests: engine unit tests + real test-renderer component tests (31 passing).
@github-actions

Copy link
Copy Markdown
Contributor

The following comment was made by an LLM, it may be inaccurate:

Potential Related PRs Found:

  1. PR feat(tui): native Arabic and RTL (bidi) support for prompts and messages #48587feat(tui): native Arabic and RTL (bidi) support for prompts and messages

  2. PR feat(tui): native bidi/RTL rendering for prompts and messages #48589feat(tui): native bidi/RTL rendering for prompts and messages

    • Related to bidi/RTL support, likely an alternative or intermediate approach to the same feature.

Why they're related: PR #48590 is a port of the bidi support from the dev branch (#48587) to the beta/opencode2 codebase. The PR description states "Refs #38524, #40004, #39525, #32984. The dev-line PR (#48587) carries the auto-closing references; this PR is the same fix for the beta line."

The TUI cannot render with its own font, so /fonts is an application-level bridge: it lists installed font families (recommended Arabic fonts first) and writes the choice into Windows Terminal or the VS Code terminal settings, keeping a timestamped backup. It never installs fonts.
- Wrap the /fonts dialog footer in <text> so the dialog no longer crashes
  with an orphan-text error and actually opens.
- Register BidiTextRenderable as the global 	ext component so RTL
  ordering/alignment works in dialogs, lists, tool output and toasts,
  not only in the prompt and message bodies. English-only content still
  takes the stock paint path.
- Paint reasoning prose through the bidi code renderer so Arabic thinking
  blocks reorder RTL while code lines without Arabic stay LTR.
- Detect the hosting terminal (Windows Terminal vs VS Code) before writing
  the Arabic font, and preserve existing fallbacks instead of replacing them.
- Add coverage for host detection and fallback merging.
Windows Terminal has no font.fallbacks setting; it resolves a
comma-separated font.face chain instead. The previous writer stored an
array the terminal silently ignored, so picking a font never changed
anything. The writer now builds 'Mono, <pick>, <arabic...>' preserving
the user's faces, and removes any stale fallbacks key.
Windows Terminal warns about every missing family in the face chain.
Limit the tail to families that resolve on this machine (the picked font
is always kept); the dialog already labels the rest as not installed.
Drop the /fonts command, its dialog, the settings writer, docs and
tests. Font selection stays where it belongs: the terminal's own
settings. Bidi rendering itself is unchanged.
- Apply UAX anomalyco#9 L2 flips over the full augmented stream including isolate
  controls, matching the reference application instead of clamping them
  away. Clamping rotated words and detached punctuation in mixed runs.
- Expand each island across adjacent Latin/digit words so one isolate
  covers every maximal LTR run; sibling sub-flips plus the whole-line
  flip otherwise swap neighboring runs.
- Broaden island seeds: markdown code spans, spaced Windows paths and
  key:line references. Code spans containing RTL prose stay natural.
- Prefer wrapping outside LTR islands so tokens are not split across
  visual lines unless unavoidable.
- Paint multi-unit graphemes with drawText: native setCell drops Arabic
  tashkeel. Shared paintBidiCell helper for text, textarea and markdown.
- Install markdown bidi paint on CodeRenderable itself so list items,
  blockquotes and streaming updates inherit it; fenced code and English
  keep the stock painter. Drop the now-redundant renderNode hook.
- Regression tests for mixed Arabic/English/code/path/URL/number runs,
  lists, links, diffs, wrapping and tashkeel in all three paint paths.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants