diff --git a/apps/web/src/components/ChatMarkdown.test.tsx b/apps/web/src/components/ChatMarkdown.test.tsx
new file mode 100644
index 00000000000..c650dc11f04
--- /dev/null
+++ b/apps/web/src/components/ChatMarkdown.test.tsx
@@ -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(
+
{renderSkillInlineMarkdownChildren(children, skills)}
; + p({ node, children, ...props }) { + return ( ++ {renderSkillInlineMarkdownChildren(children, skills)} +
+ ); }, li({ node, children, ...props }) { const listItemStart = node?.position?.start.offset; const markerOffset = typeof listItemStart === "number" ? findTaskListMarkerOffset(text, listItemStart) : null; return ( -+ {children} ++ ); + }, input({ node: _node, type, checked, disabled: _disabled, ...props }) { if (type !== "checkbox" || !onTaskListChange) { return ( diff --git a/apps/web/src/components/ComposerPromptEditor.tsx b/apps/web/src/components/ComposerPromptEditor.tsx index 18579cda6d3..dd4cc1af3d3 100644 --- a/apps/web/src/components/ComposerPromptEditor.tsx +++ b/apps/web/src/components/ComposerPromptEditor.tsx @@ -63,6 +63,7 @@ import { INLINE_TERMINAL_CONTEXT_PLACEHOLDER, type TerminalContextDraft, } from "~/lib/terminalContext"; +import { dirFor } from "~/lib/rtl"; import { cn } from "~/lib/utils"; import { basenameOfPath } from "~/pierre-icons"; import { @@ -1383,6 +1384,25 @@ function ComposerSurroundSelectionPlugin(props: { return null; } +function ComposerBidiDirectionPlugin() { + const [editor] = useLexicalComposerContext(); + useEffect(() => { + const syncRootDirection = (text: string) => { + const root = editor.getRootElement(); + if (!root) return; + const dir = dirFor(text); + if (root.getAttribute("dir") !== dir) root.setAttribute("dir", dir); + }; + + editor.getEditorState().read(() => { + syncRootDirection($getRoot().getTextContent()); + }); + + return editor.registerTextContentListener(syncRootDirection); + }, [editor]); + return null; +} + function ComposerPromptEditorInner({ value, cursor, @@ -1610,7 +1630,7 @@ function ComposerPromptEditorInner({ contentEditable={