Skip to content
dsh.fish
Bundle

dsh-client-ui-rtl

Right-to-left text direction for the DeepSeek Harness Web client. Marks Arabic, Hebrew, Persian, Urdu and Syriac content as RTL by script dominance, leaving Latin content untouched.

Source
haythamat
stars
1 stars
License
MIT
Updated
Updated yesterday

Readme

# dsh-client-ui-rtl

Right-to-left text direction for the DeepSeek Harness Web client.

The client renders left-to-right by default. Arabic, Hebrew, Persian, Urdu and
other right-to-left content therefore arrives correct as text but wrong on
screen: bullets sit on the left, table columns run the wrong way, and any
sentence mixing Latin and RTL words is reordered against its meaning.

This package marks right-to-left content as it renders, and leaves everything
else alone.

## Before and after

An Arabic answer mixing English technical terms:

| Before | After |
|---|---|
| ![Arabic answer rendered left-to-right, bullets and table misaligned](docs/before-answer.png) | ![The same answer rendered right-to-left](docs/after-answer.png) |

A short prompt opening with a Latin product name — the case `dir="auto"` gets
wrong:

| Before | After |
|---|---|
| ![Arabic prompt reordered because it begins with an English word](docs/before-question.png) | ![The same prompt rendered right-to-left](docs/after-question.png) |

## Install

```
dsh plugin --profile web add github:haythamat/dsh-client-ui-rtl
```

Restart `dsh web` afterwards. The package ships its own patch layer, so no
configuration file needs editing.

## Why not `dir="auto"`

`dir="auto"` resolves direction from the **first strong character** in the
element. That works for most Hebrew and Arabic text, but it fails on a pattern
that is very common in technical writing in both languages: a sentence opening
with an English product name. The first strong character is Latin, the whole
paragraph resolves left-to-right, and every word after it is misordered.

## The estimator contract

Direction is estimated by script dominance. This is a **product heuristic, not
universal direction detection** — it is stated explicitly here so it can be
tested, versioned, and disagreed with.

- **Unit** — one whitespace-delimited token. Punctuation does not split a
  token, so identifiers, paths and package names count once rather than once
  per segment.
- **Classification** — a token holding any right-to-left character is an RTL
  word; otherwise a token holding any Latin letter is an LTR word. Mixed tokens
  resolve to RTL, since RTL prose embeds Latin terms far more often than the
  reverse.
- **Neutral** — tokens with no strong letters (numbers, punctuation, symbols)
  count as neither.
- **Tie** — an equal count resolves to RTL.
- **Fallback** — a block with no RTL words at all is left untouched, so
  left-to-right content is never marked.
- **Override** — an element carrying a `dir` attribute this package did not set
  is left as authored. That is the opt-out.

Counting words rather than characters matters because RTL words are short while
Latin technical terms are long: `اشرح لي ال Agentic AI` is eight Arabic
characters against nine Latin ones, but three Arabic words against two Latin
ones.

Elements are judged on the text they hold **directly**, not on their
descendants, so a wrapper containing many children is not flipped on account of
its contents, and an inline `code` span inside a paragraph does not contribute
Latin words. Tables and lists are the exception: they are judged on their whole
subtree, because column order and list markers only reorder when the container
itself flips.

## Known failures

Word dominance has irreducible failure modes. Documented rather than hidden:

- **Short RTL clause, long Latin command.** `شغّل npx @deepseek-ai/dsh web` is
  one Arabic word against three Latin tokens and resolves left-to-right, which
  is wrong. No word-count rule fixes this; it needs authored direction or
  inline isolation of the command.
- **Standalone parenthetical.** A block that is mostly a Latin term in
  parentheses resolves left-to-right even inside RTL prose.
- **Evenly balanced blocks** resolve to RTL by the tie rule, which is a choice,
  not a derivation.
- **Text split across child elements.** Direction is decided per element on the
  text it holds directly, so a wrapper whose text lives entirely in children is
  never evaluated — no element sees the whole sentence. A bubble rendering as
  `<div><div>كيف أستخدم</div><div>dsh-client-ui-brand-official</div></div>`
  leaves the identifier child correctly LTR in isolation while the sentence it
  belongs to is never judged. This is a placement problem, not an estimator
  problem; inheriting direction at the root with container-level overrides
  avoids it by construction.  

Set `dir` explicitly on any block the estimator gets wrong; the package will
leave it alone.

## What is left alone

`code`, `pre`, `kbd`, `samp`, `var`, form controls, and embedded media are never
marked — their direction is meaningful as authored. No bidi control characters
are inserted into content; the package only sets DOM attributes.

## Streaming

A `MutationObserver` watches for added nodes and changed text, batched to one
pass per animation frame, so assistant messages are corrected as they stream
rather than only on load. Elements are reconciled rather than marked once: a
block whose balance shifts while it streams has its direction withdrawn or
applied to match.

Attribute mutations are deliberately not observed, since this package writes
attributes and watching them would feed its own writes back in.

## Model Experience

None, as the package contributes browser presentation only; nothing here reaches
a model request.

#### KV Cache effect

None; this package neither assembles nor sends a provider request.

## Known Limitations and Deferred Work

- **Presentation only, not layout** — the shell itself stays left-to-right. The
  sidebar remains on the left and controls keep their positions. Mirroring the
  application shell belongs in a separate package occupying the layout slots.
- **The composer is untouched** — the input textarea is deliberately excluded to
  avoid disturbing IME and selection state.
- **The browser title is independent** — `DSH_CLIENT_TITLE` selects title text at
  build time rather than through a UI slot.
- **Validated in Arabic only.** Hebrew, Persian, Syriac and Urdu are in the
  character ranges but have not been reviewed by fluent readers of those
  languages. A DOM attribute cannot prove readability.

## License

MIT

Install

dsh plugin --profile web add github:haythamat/dsh-client-ui-rtl

Profile: web

  • This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.
Source