- Startseite
- Plugins
- UI-Erweiterungen
- dsh-plugin-rtl-text
dsh-plugin-rtl-text
mojtabafaraji/dsh-plugin-rtl-text
DeepSeek Harness plugin that fixes RTL/LTR bidi rendering in chat: per-block direction for Persian and Arabic messages with embedded English, plus list markers and quote bars that follow their text
Installation
dsh plugin --profile web add github:mojtabafaraji/dsh-plugin-rtl-textREADME
dsh-plugin-rtl-text
A DeepSeek Harness (Cordis) plugin that fixes mixed right-to-left / left-to-right text rendering in chat messages on the Web and Desktop GUI.
When a reply mixes Persian or Arabic with English words, code, or numbers, an LTR page reorders the line: sentences read backwards, punctuation attaches to the wrong side, list numbers and bullets stay on the left edge, and quote bars sit on the wrong side. This plugin gives every chat block a direction computed from its own content — without changing a single character of message text.
Contents
- What it fixes
- What it does not change
- Installation
- How it works
- Compatibility
- Configuration
- Where is cordis.yml?
- Troubleshooting
- Security and privacy
- Example
- Testing
- Building from source
- Publishing a release
- Package contents
- Author
- License
What it fixes
- Persian or Arabic paragraphs with embedded English (
سلام، how are you?) lay out RTL, with the English runs correct inside them. - English-led Persian sentences (
build سبز + ۴ تست پاس …) that a first-character heuristic would read as LTR. - Sentences that open Persian but contain many Latin technical tokens (
سطح رندر: تابع واقعی renderIndexInjections …) — they are never flipped to LTR. - List markers and quote bars:
olnumbers,ulbullets, andblockquotebars move to the right together with their content. - Existing history as well as live replies: the fix is one stylesheet row and one script, so every already-loaded conversation benefits immediately — no re-streaming.
What it does not change
- Message text. The durable session log, model history, and CLI output stay byte-identical; the plugin only touches presentation.
- Other profiles. Headless and TUI profiles have no GUI to style — the injected rows are never collected there and the plugin is a harmless no-op.
- Code blocks.
preblocks are deliberately out of scope and render as authored.
Installation
The plugin is installed as a bundle layer into a profile — this is what actually mounts it:
# from the npm registry
pnpm dsh plugin --profile <name> add dsh-plugin-rtl-text
# from a local checkout (no registry needed)
pnpm dsh plugin --profile <name> add <path-to-this-package>
You can also use the GUI Add plugin button with the package name. A plain pnpm add only copies files into the profile's node_modules; it does not append the bundle layer, so the plugin would never mount — prefer the commands above.
Peer dependency: @deepseek-ai/cordis (published on npm, and already present in every Harness installation).
Why the package declares dsh.bundle
The Harness plugin manager refuses packages without dsh.bundle with declares no dsh.bundle, because there would be no patch file to add to the profile's composition. This package declares:
"dsh": {
"bundle": {
"patch": "./cordis.patch.yml"
}
}
The install does three things:
pnpm adds the package into<DSH_HOME>/profiles/<name>/and lists it in the profilepackage.jsondependencies.- Appends
dsh-plugin-rtl-texttodsh.profile.bundlesin the profile manifest. - Contributes the package's
cordis.patch.ymlas a composition layer:
- insert:
- id: rtl-text
name: dsh-plugin-rtl-text
config:
enabled: true
Verify the composition with pnpm dsh --profile <name> --dump-config — the output contains a == dsh-plugin-rtl-text layer with the id: rtl-text entry.
After installing into a running profile
Injected rows are collected from the live, mounted plugin at page load, so:
- Remount the plugin — toggle it off and on in the GUI plugin page, or restart the host.
- Reload the page.
index.htmlis rendered per request and picks up the rows.
Updating an existing install (a new build or a new registry version) needs the same two steps.
How it works
The Web markdown renderer deliberately keeps raw HTML inert — no HTML parser enters its pipeline — so a display fix cannot travel inside message text. Instead, the plugin uses the Harness index-injection extension point (webserver/index-inject, the same event ui-theme uses): on every page load the webserver collects injection rows and renders them into index.html, and this plugin appends two rows.
The style row
The no-JavaScript default: each block takes its direction from its first strong character (unicode-bidi: plaintext), scoped to chat rows through their stable data-chat-* attributes:
:where([data-chat-group-key],[data-chat-flow-key]) :is(p,li,h1,h2,h3,h4,h5,h6,blockquote,dd,dt,td,th,figcaption){unicode-bidi:plaintext}
Sentences that open in their dominant language resolve correctly with no script at all.
The script row: Latin-led Persian sentences
First-strong alone reads build سبز + ۴ تست پاس as LTR because it opens with English. When a text block opens LTR yet strong RTL characters (Hebrew/Arabic blocks, including Arabic punctuation and diacritics) outnumber strong LTR ones (Latin/Greek/Cyrillic), the script pins dir="rtl" with an inline unicode-bidi: isolate so the explicit direction wins over the stylesheet. Sentences that open Persian are never pinned — technical Latin tokens such as renderIndexInjections must not flip سطح رندر: تابع واقعی … to LTR, so first-strong keeps them RTL.
Container direction: list numbers, bullets, quote bars
Markers follow the container's own direction property, which unicode-bidi: plaintext on the inner text cannot change — on an LTR page they would stay on the left edge. For chat-scoped ul, ol, and blockquote the script applies the full rule (first-strong RTL, or RTL majority regardless of the opening character) and pins an inline direction: rtl, so a list opening 1. اولین مورد … keeps its numbers on the right with the text. English lists stay left.
A MutationObserver reapplies every decision as streaming text grows, removes a stale pin when the text no longer qualifies, and watches text and child mutations only — attribute writes never loop.
What is never touched: message text itself. The durable session log, model history, and CLI output stay byte-identical, and messages already in history are fixed too because the mechanism is presentation-only.
Compatibility
- Requires a DeepSeek Harness release that exposes
webserver/index-injectand chat rows carryingdata-chat-group-key/data-chat-flow-key— current releases do. - Both extension points are pre-stable public APIs. When upgrading the Harness across versions, rerun
pnpm run testand check that chat rows still carry those attributes; a Harness-side rename would need a matching selector update here. - Applies to the Web GUI and the Desktop GUI (the same document). Headless and TUI profiles are unaffected.
Configuration
| Field | Default | Meaning |
|---|---|---|
enabled | true | Set false to keep the plugin mounted without contributing rows. |
Override it in the profile's cordis.patch.yml with an id-targeted patch:
- id: rtl-text
config:
enabled: false
Where is cordis.yml?
Three different files share this name:
<profile>/cordis.yml— the profile's root entry list. It stays empty; the file header says to editcordis.patch.ymlinstead.<profile>/cordis.patch.yml— the profile's own patch layer (id-targeted overrides anddisabledflags), applied above every bundle layer.cordis.patch.ymlinside this package — this plugin's bundle layer, shipped with the package and referenced bydsh.bundle.patch. The loader reads it when the package is installed as a bundle; you do not edit it.
Troubleshooting
| Symptom | What to do |
|---|---|
| Installed and reloaded, but nothing changed | Remount first (plugin page: off → on, or restart the host), then reload. Rows are collected from the live mounted plugin at page load. |
| One sentence still has the wrong direction | Heuristic limit: a nearly balanced bilingual sentence falls back to its first strong character. Open an issue with the exact sentence. |
| List numbers or bullets still on the left | Confirm the reload followed the remount. If your Harness version renamed the chat row attributes, the selector needs a matching update (see Compatibility). |
| You want it off | Set enabled: false as shown in Configuration. |
Security and privacy
- The plugin contributes two static rows — one CSS block and one inline script — built from constants inside the package. No message content is embedded, no network requests are made, and nothing is stored.
- The script reads
textContentof chat blocks and writesdir/inline style on them; aMutationObserveronly watches for new content. Nothing leaves the page. - Because message text is never rewritten, transcripts and model history stay byte-identical.
Example
Page direction LTR:
- Reply
سلام، how are you?: before, the paragraph follows the page direction and runs and punctuation reappear in the wrong order; after, the direction comes fromسلام— a full RTL line withhow are yourendered LTR inside it. - A list item starting
build سبز + ۴ تست پاس …: first-strong reads the openingbuildas LTR and breaks the Persian sentence; the script counts the Persian letters as the majority and pinsdir="rtl"for the whole block. سطح رندر: تابع واقعی renderIndexInjections → style در <head> …: first-strong already reads the openingسطحas RTL, so the script stays out of it — the Latin technical tokens outnumber the Persian letters, but a majority override here would wrongly flip the sentence to LTR.- A list or quote starting with a number (
1. اولین مورد …) or a bullet: the marker follows the container's direction, not the text's — the script pins theul/ol/blockquoteitself withdir="rtl"and an inlinedirection: rtl, so the numbers and bullets move to the right with their items.
Testing
pnpm run test # node:test suite against a real Cordis Context
The suite covers both injection rows (preserving other rows, never emitting </script), the pinning rules against both reported failure directions (English-led Persian forced RTL; Persian-led Latin-heavy left to first-strong), container pinning for lists and quotes (numbers, bullets, quote bars), stale-pin removal, the enabled: false configuration, listener unwind on fiber.dispose(), and two-rows-per-collection semantics.
Building from source
pnpm run build # tsdown → lib/index.js + lib/index.d.ts
Run this inside the package directory only. The DeepSeek Harness repository root has its own monorepo tsdown.config.ts which expects tsc-emitted lib/types/{index,invariant,startup}.js entries; building from the repository root picks up that config instead of the package-local one and fails.
The package has no runtime dependencies, so a pnpm install of this package alone is not required. Building and testing need tsdown and @deepseek-ai/cordis, which resolve from an adjacent DeepSeek Harness checkout — this is where development happens.
Publishing a release
- Green checks:
pnpm run buildandpnpm run test(from a checkout inside the Harness tree, as above). - Inspect the tarball:
npm pack --dry-run— expectlib/index.js,lib/index.d.ts,cordis.patch.yml,README.md,CHANGELOG.md, andLICENSE. - Bump
versionand add an entry to CHANGELOG.md. npm loginthennpm publish—prepublishOnlyrebuildslib/automatically.- Push to GitHub (MojtabaFaraji/dsh-plugin-rtl-text): the repository commits
lib/on purpose so a plain clone works without the Harness toolchain;.gitignoreexcludes onlynode_modules/, local npm caches, tarballs, and logs.
Package contents
| File | Purpose |
|---|---|
lib/index.js | runtime entry (apply) |
lib/index.d.ts | type declarations (apply, Config) |
cordis.patch.yml | the bundle patch layer (dsh.bundle.patch) |
README.md | this file |
CHANGELOG.md | release history |
LICENSE | MIT license text |
Author
Mojtaba Faraji
License
MIT — see LICENSE.
Ähnliche Plugins
dsh-web (dsh-task-board)
zhu1090093659/dsh-web
dsh-web (dsh-web-all)
zhu1090093659/dsh-web
dsh-web-ui (dsh-task-board)
zhu1090093659/dsh-web-ui
dsh-web-ui (dsh-web-ui-all)
zhu1090093659/dsh-web-ui