-
Notifications
You must be signed in to change notification settings - Fork 2.7k
[codex] Fix RTL chat message rendering #3674
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,74 @@ | ||
| import { EnvironmentId } from "@t3tools/contracts"; | ||
| import { renderToStaticMarkup } from "react-dom/server"; | ||
| import { describe, expect, it, vi } from "vite-plus/test"; | ||
|
|
||
| import ChatMarkdown from "./ChatMarkdown"; | ||
|
|
||
| vi.mock("@effect/atom-react", () => ({ | ||
| useAtomValue: () => ({ availableEditors: [] }), | ||
| })); | ||
|
|
||
| vi.mock("../hooks/useTheme", () => ({ | ||
| useTheme: () => ({ resolvedTheme: "light" }), | ||
| })); | ||
|
|
||
| vi.mock("../editorPreferences", () => ({ | ||
| useOpenInPreferredEditor: () => () => {}, | ||
| })); | ||
|
|
||
| vi.mock("../state/assets", () => ({ | ||
| assetEnvironment: { createUrl: {} }, | ||
| })); | ||
|
|
||
| vi.mock("../state/entities", () => ({ | ||
| useActiveEnvironmentId: () => EnvironmentId.make("environment-local"), | ||
| })); | ||
|
|
||
| vi.mock("../state/preview", () => ({ | ||
| previewEnvironment: { open: {} }, | ||
| })); | ||
|
|
||
| vi.mock("../state/server", () => ({ | ||
| serverEnvironment: { configValueAtom: () => ({}) }, | ||
| })); | ||
|
|
||
| vi.mock("../state/session", () => ({ | ||
| usePreparedConnection: () => ({ _tag: "None" }), | ||
| })); | ||
|
|
||
| vi.mock("../state/use-atom-command", () => ({ | ||
| useAtomCommand: () => () => {}, | ||
| })); | ||
|
|
||
| vi.mock("../state/use-atom-query-runner", () => ({ | ||
| useAtomQueryRunner: () => () => "", | ||
| })); | ||
|
|
||
| describe("ChatMarkdown bidi list rendering", () => { | ||
| it("renders code-prefixed Hebrew unordered lists as RTL", () => { | ||
| const markup = renderToStaticMarkup( | ||
| <ChatMarkdown | ||
| text={[ | ||
| "- **`apps/web`**: אפליקציית React/Vite שמציגה צ'אט", | ||
| "- **`apps/server`**: שרת Node.js שמריץ CLI", | ||
| ].join("\n")} | ||
| cwd={undefined} | ||
| />, | ||
| ); | ||
|
|
||
| expect(markup).toContain('<ul dir="rtl">'); | ||
| expect(markup).toContain('<li dir="rtl">'); | ||
| }); | ||
|
|
||
| it("keeps English unordered lists LTR", () => { | ||
| const markup = renderToStaticMarkup( | ||
| <ChatMarkdown | ||
| text={["- README.md", "- apps/server/package.json"].join("\n")} | ||
| cwd={undefined} | ||
| />, | ||
| ); | ||
|
|
||
| expect(markup).toContain('<ul dir="ltr">'); | ||
| expect(markup).toContain('<li dir="ltr">'); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -32,6 +32,7 @@ import { | |
| workLogEntryIsToolLike, | ||
| } from "../../session-logic"; | ||
| import { type TurnDiffSummary } from "../../types"; | ||
| import { dirFor } from "../../lib/rtl"; | ||
| import { summarizeTurnDiffStats } from "../../lib/turnDiffTree"; | ||
| import { | ||
| getRenderablePatch, | ||
|
|
@@ -1561,7 +1562,10 @@ const UserMessageBody = memo(function UserMessageBody(props: { | |
| } | ||
|
|
||
| return ( | ||
| <div className="whitespace-pre-wrap wrap-break-word text-sm leading-relaxed text-foreground"> | ||
| <div | ||
| dir={dirFor(props.text)} | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Medium
Also found in 1 other location(s)
🤖 Copy this AI Prompt to have your agent fix this: |
||
| className="bidi-plaintext whitespace-pre-wrap wrap-break-word text-sm leading-relaxed text-foreground" | ||
| > | ||
| {inlineNodes} | ||
| </div> | ||
| ); | ||
|
|
@@ -1599,7 +1603,10 @@ const UserMessageBody = memo(function UserMessageBody(props: { | |
| } | ||
|
|
||
| return ( | ||
| <div className="whitespace-pre-wrap wrap-break-word text-sm leading-relaxed text-foreground"> | ||
| <div | ||
| dir={dirFor(props.text)} | ||
| className="bidi-plaintext whitespace-pre-wrap wrap-break-word text-sm leading-relaxed text-foreground" | ||
| > | ||
| {inlineNodes} | ||
| </div> | ||
| ); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -472,12 +472,12 @@ label:has(> select#reasoning-effort) select { | |
| } | ||
|
|
||
| .chat-markdown ul { | ||
| padding-left: 1.25rem; | ||
| padding-inline-start: 1.25rem; | ||
| list-style-type: disc; | ||
| } | ||
|
|
||
| .chat-markdown ol { | ||
| padding-left: 1.25rem; | ||
| padding-inline-start: 1.25rem; | ||
| list-style-type: decimal; | ||
| } | ||
|
|
||
|
|
@@ -507,7 +507,8 @@ label:has(> select#reasoning-effort) select { | |
| } | ||
|
|
||
| .chat-markdown li.task-list-item input[type="checkbox"] { | ||
| margin: 0 0.35em 0.15em -1.25rem; | ||
| margin-block: 0 0.15em; | ||
| margin-inline: -1.25rem 0.35em; | ||
| vertical-align: middle; | ||
| } | ||
|
|
||
|
|
@@ -537,11 +538,39 @@ label:has(> select#reasoning-effort) select { | |
| } | ||
|
|
||
| .chat-markdown blockquote { | ||
| border-left: 2px solid var(--border); | ||
| padding-left: 0.8rem; | ||
| border-inline-start: 2px solid var(--border); | ||
| padding-inline-start: 0.8rem; | ||
| color: var(--muted-foreground); | ||
| } | ||
|
|
||
| /* Bidi: each line resolves its own base direction (UAX#9 plaintext). */ | ||
| .bidi-plaintext { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Medium
🤖 Copy this AI Prompt to have your agent fix this: |
||
| unicode-bidi: plaintext; | ||
| } | ||
|
|
||
| /* Per-line direction inside markdown text blocks. */ | ||
| .chat-markdown p, | ||
| .chat-markdown li, | ||
| .chat-markdown h1, | ||
| .chat-markdown h2, | ||
| .chat-markdown h3, | ||
| .chat-markdown h4, | ||
| .chat-markdown h5, | ||
| .chat-markdown h6, | ||
| .chat-markdown blockquote { | ||
| unicode-bidi: plaintext; | ||
| } | ||
|
|
||
| /* Source code stays LTR even inside RTL messages. */ | ||
| .chat-markdown pre, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Medium Setting 🤖 Copy this AI Prompt to have your agent fix this: |
||
| .chat-markdown code, | ||
| .chat-markdown .chat-markdown-codeblock, | ||
| .chat-markdown .chat-markdown-shiki, | ||
| .chat-markdown .chat-markdown-table-container { | ||
| direction: ltr; | ||
| unicode-bidi: isolate; | ||
| } | ||
|
|
||
| .chat-markdown section[data-footnotes] { | ||
| margin-top: 1.25rem; | ||
| border-top: 1px solid var(--border); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🟡 Medium
components/ChatMarkdown.tsx:1427The
ul,ol, andlicomponent overrides computedirby callingblockDirForMarkdownNode(text, node, children), but a list node'spositionspans the entire subtree including nested lists. A top-level English list that contains a long nested Hebrew/Arabic sublist will resolve the outer<ul>/<ol>and parent<li>asdir="rtl", placing the top-level bullets/numbers on the wrong side even though the parent items are LTR. Consider computing the direction from only the node's own direct text rather than the full subtree range.🤖 Copy this AI Prompt to have your agent fix this: