Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
71 commits
Select commit Hold shift + click to select a range
712f715
fix(chat): align messages with their text direction
ShlomiPorush Aug 19, 2026
4f0ab2a
fix(chat): preserve RTL markdown layout
ShlomiPorush Aug 19, 2026
364f2cb
fix(web): mirror alert chrome in RTL
ShlomiPorush Aug 19, 2026
49b039b
Merge remote-tracking branch 'origin/main' into fix/chat-message-dire…
ShlomiPorush Aug 31, 2026
8f8a069
fix(mobile): parse markdown fences before RTL detection
ShlomiPorush Aug 31, 2026
ea22c78
fix(mobile): align RTL markdown containers
ShlomiPorush Aug 31, 2026
39da1ee
fix(chat): preserve RTL direction in markdown edge cases
ShlomiPorush Aug 31, 2026
4adb4fe
fix(chat): ignore markdown code during direction detection
ShlomiPorush Aug 31, 2026
c094fe0
refactor(mobile): derive text direction from markdown AST
ShlomiPorush Aug 31, 2026
028c681
fix(mobile): keep RTL list markers at inline start
ShlomiPorush Aug 31, 2026
ef11976
fix(web): align markdown details summaries logically
ShlomiPorush Aug 31, 2026
ba1a24b
fix(web): isolate skill labels from message direction
ShlomiPorush Aug 31, 2026
f124b9f
fix(web): isolate artifact cards from message direction
ShlomiPorush Aug 31, 2026
2eb6914
Merge origin/main into fix/chat-message-direction
haithamassoli44 Sep 8, 2026
6492bb5
Merge branch 'main' into fix/chat-message-direction
haithamassoli Sep 8, 2026
0527c5b
Merge branch 'main' into fix/chat-message-direction
haithamassoli Sep 10, 2026
da9bcf6
Merge branch 'main' into fix/chat-message-direction
haithamassoli Sep 10, 2026
95ffbb1
Merge branch 'main' into fix/chat-message-direction
haithamassoli Sep 12, 2026
0445a49
fix(chat): direct composer questions by content
haithamassoli44 Sep 13, 2026
3a500a7
Merge branch 'main' into fix/chat-message-direction
haithamassoli Sep 13, 2026
50a1993
fix(chat): detect RTL-dominant mixed text
haithamassoli44 Sep 13, 2026
37106ad
Merge branch 'main' into fix/chat-message-direction
haithamassoli Sep 13, 2026
def51aa
Merge branch 'main' into fix/chat-message-direction
haithamassoli Sep 13, 2026
a83fab1
Merge branch 'main' into fix/chat-message-direction
haithamassoli Sep 13, 2026
f78aa08
Merge branch 'main' into fix/chat-message-direction
haithamassoli Sep 13, 2026
6a98f73
Merge branch 'main' into fix/chat-message-direction
haithamassoli Sep 13, 2026
7b33641
Merge branch 'main' of github.com:pingdotgg/t3code into fix/chat-mess…
haithamassoli44 Sep 14, 2026
19ec4a2
Merge branch 'main' into fix/chat-message-direction
haithamassoli Sep 14, 2026
874fd64
Merge branch 'main' into fix/chat-message-direction
haithamassoli44 Sep 14, 2026
c5ddfa3
Merge branch 'main' into fix/chat-message-direction
haithamassoli Sep 15, 2026
32ce82a
Merge branch 'main' into fix/chat-message-direction
haithamassoli Sep 15, 2026
7cb9c28
Merge branch 'main' into fix/chat-message-direction
haithamassoli Sep 15, 2026
e5b37e9
Merge branch 'main' into fix/chat-message-direction
haithamassoli Sep 15, 2026
b1ab514
Merge branch 'main' into fix/chat-message-direction
haithamassoli44 Sep 15, 2026
7b03def
Merge branch 'main' into fix/chat-message-direction
haithamassoli44 Sep 16, 2026
641c6bc
Merge branch 'main' into fix/chat-message-direction
haithamassoli44 Sep 16, 2026
913ef37
Merge branch 'main' into fix/chat-message-direction
haithamassoli Sep 17, 2026
d733822
Merge branch 'main' into fix/chat-message-direction
haithamassoli44 Sep 17, 2026
e068479
Merge branch 'main' into fix/chat-message-direction
haithamassoli44 Sep 20, 2026
f6d6422
Merge branch 'main' into fix/chat-message-direction
haithamassoli44 Sep 20, 2026
6dccbca
Merge branch 'main' into fix/chat-message-direction
haithamassoli Sep 20, 2026
61c5e30
Merge branch 'main' into fix/chat-message-direction
haithamassoli Sep 21, 2026
03000d3
Merge branch 'main' into fix/chat-message-direction
haithamassoli Sep 21, 2026
171afe7
Merge branch 'main' into fix/chat-message-direction
haithamassoli Sep 21, 2026
7d83025
Merge branch 'main' into fix/chat-message-direction
haithamassoli44 Sep 21, 2026
05fe6f7
Merge branch 'main' into fix/chat-message-direction
haithamassoli Sep 21, 2026
2ca6183
Merge branch 'main' into fix/chat-message-direction
haithamassoli Sep 21, 2026
45c3ca6
Merge branch 'main' into fix/chat-message-direction
haithamassoli Sep 21, 2026
bd3c06f
Merge branch 'main' into fix/chat-message-direction
haithamassoli44 Sep 22, 2026
b3170ba
Merge branch 'main' into fix/chat-message-direction
haithamassoli44 Sep 22, 2026
8840efd
Merge branch 'main' into fix/chat-message-direction
haithamassoli Sep 22, 2026
96d40b3
Merge remote-tracking branch 'origin/main' into fix/chat-message-dire…
haithamassoli44 Sep 23, 2026
ed50c1e
Merge branch 'main' into fix/chat-message-direction
haithamassoli44 Sep 23, 2026
d60f80c
Merge branch 'main' into fix/chat-message-direction
haithamassoli Sep 24, 2026
d6b10a4
Merge branch 'main' into fix/chat-message-direction
haithamassoli Sep 24, 2026
54cd408
Merge branch 'main' into fix/chat-message-direction
haithamassoli Sep 24, 2026
aa8c488
Merge remote-tracking branch 'origin/main' into fix/chat-message-dire…
haithamassoli44 Sep 26, 2026
6a3937f
Merge branch 'main' into fix/chat-message-direction
haithamassoli Sep 26, 2026
fc3ec70
Merge branch 'main' into fix/chat-message-direction
haithamassoli Sep 26, 2026
2679c6f
Merge branch 'main' into fix/chat-message-direction
haithamassoli Sep 26, 2026
bdaa341
Merge branch 'main' into fix/chat-message-direction
haithamassoli Sep 26, 2026
c44064f
Merge branch 'main' into fix/chat-message-direction
haithamassoli Sep 26, 2026
e2f5c80
Merge branch 'main' into fix/chat-message-direction
haithamassoli44 Sep 27, 2026
567359f
Merge branch 'main' into fix/chat-message-direction
haithamassoli Sep 28, 2026
2ef2db2
Merge branch 'main' into fix/chat-message-direction
haithamassoli Sep 29, 2026
fdefc58
Merge branch 'main' into fix/chat-message-direction
haithamassoli Sep 29, 2026
62a5ced
Merge branch 'main' into fix/chat-message-direction
haithamassoli Sep 29, 2026
47af104
Merge branch 'main' into fix/chat-message-direction
haithamassoli Sep 29, 2026
c1eda04
Merge branch 'main' into fix/chat-message-direction
haithamassoli Sep 30, 2026
ad5c1c1
Merge branch 'main' into fix/chat-message-direction
haithamassoli44 Sep 30, 2026
e128403
Merge branch 'main' into fix/chat-message-direction
haithamassoli Sep 30, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 113 additions & 32 deletions apps/mobile/src/features/threads/ThreadFeed.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ import {
} from "react";
import {
Markdown,
parseMarkdown,
type CustomRenderers,
type NodeStyleOverrides,
type PartialMarkdownTheme,
Expand Down Expand Up @@ -97,6 +98,7 @@ import { copyTextWithHaptic } from "../../lib/copyTextWithHaptic";
import { tryOpenExternalUrl } from "../../lib/openExternalUrl";
import { downloadAndShareAttachment } from "../../lib/attachmentDownload";
import { hasWideMarkdownBlock } from "../../lib/wideMarkdownBlocks";
import { resolveMarkdownNodeTextDirection, type TextDirection } from "../../lib/textDirection";
import { faviconUrlForOrigin } from "@t3tools/shared/favicon";
import {
hasNativeSelectableMarkdownText,
Expand Down Expand Up @@ -615,7 +617,7 @@ interface MarkdownStyleSets {
interface MarkdownStyleSet {
readonly theme: PartialMarkdownTheme;
readonly styles: NodeStyleOverrides;
readonly renderers: CustomRenderers;
readonly renderers: Readonly<Record<TextDirection, CustomRenderers>>;
readonly nativeTextStyle: NativeMarkdownTextStyle;
}

Expand Down Expand Up @@ -699,6 +701,8 @@ function MarkdownInlineCode(props: {
color: presentation ? props.textColor : props.codeColor,
fontSize: props.fontSize,
lineHeight: props.lineHeight,
textAlign: "left",
writingDirection: "ltr",
}}
>
{presentation ? (
Expand Down Expand Up @@ -772,6 +776,65 @@ function ArtifactTemplateCard(props: {
);
}

const NitroMarkdownMessage = memo(function NitroMarkdownMessage(props: {
readonly text: string;
readonly markdownStyles: MarkdownStyleSet;
}) {
const sourceAst = useMemo(() => parseMarkdown(props.text, { gfm: true }), [props.text]);
const direction = resolveMarkdownNodeTextDirection(sourceAst);
const styles = useMemo<NodeStyleOverrides>(
() => ({
...props.markdownStyles.styles,
document: {
...props.markdownStyles.styles.document,
direction,
},
paragraph: {
...props.markdownStyles.styles.paragraph,
direction,
},
list: {
...props.markdownStyles.styles.list,
direction,
},
list_item: {
...props.markdownStyles.styles.list_item,
direction,
},
task_list_item: {
...props.markdownStyles.styles.task_list_item,
direction,
},
blockquote: {
...props.markdownStyles.styles.blockquote,
direction,
...(direction === "rtl"
? {
borderLeftWidth: 0,
borderRightWidth: props.markdownStyles.styles.blockquote?.borderLeftWidth,
borderRightColor: props.markdownStyles.styles.blockquote?.borderLeftColor,
paddingLeft: 0,
paddingRight: props.markdownStyles.styles.blockquote?.paddingLeft,
}
: null),
},
}),
[direction, props.markdownStyles.styles],
);

return (
<Markdown
options={{ gfm: true }}
renderers={props.markdownStyles.renderers[direction]}
sourceAst={sourceAst}
styles={styles}
theme={props.markdownStyles.theme}
>
{props.text}
</Markdown>
);
});

/** Tap opens a link; long-press on a native file chip shows its menu. Built once per feed. */
interface MarkdownLinkHandlers {
readonly onLinkPress: (href: string) => void;
Expand Down Expand Up @@ -815,15 +878,11 @@ const AssistantMarkdownContent = memo(function AssistantMarkdownContent(props: {
renderImage={props.renderImage}
/>
) : (
<Markdown
<NitroMarkdownMessage
key={`markdown:${segment.sourceOffset}`}
options={{ gfm: true }}
renderers={props.markdownStyles.renderers}
styles={props.markdownStyles.styles}
theme={props.markdownStyles.theme}
>
{markdown}
</Markdown>
text={markdown}
markdownStyles={props.markdownStyles}
/>
);
});
});
Expand Down Expand Up @@ -894,6 +953,8 @@ function MarkdownCodeBlock(props: {
color: props.textColor,
fontSize: props.fontSize,
lineHeight: props.lineHeight,
textAlign: "left",
writingDirection: "ltr",
Comment thread
haithamassoli44 marked this conversation as resolved.
...(Platform.OS === "android" ? { includeFontPadding: false } : null),
}}
>
Expand Down Expand Up @@ -1081,6 +1142,7 @@ function useMarkdownStyles(
copyTintColor: ColorValue,
preserveSoftBreaks: boolean,
highlightCode: boolean,
direction: TextDirection,
): CustomRenderers => ({
link: ({ children, href = "" }) => {
const presentation = resolveMarkdownLinkPresentation(href);
Expand Down Expand Up @@ -1133,7 +1195,7 @@ function useMarkdownStyles(
);
},
list: ({ node, Renderer, ordered = false, start = 1 }) => (
<View className="mt-0.5 mb-2">
<View className="mt-0.5 mb-2" style={{ direction }}>
{node.children?.map((child, index) => {
const childKey = `${child.type}:${child.beg ?? "unknown"}:${child.end ?? "unknown"}`;
if (child.type === "task_list_item") {
Expand All @@ -1147,11 +1209,12 @@ function useMarkdownStyles(
className="font-sans"
style={{
width: ordered ? 22 : 12,
marginRight: 5,
marginLeft: direction === "rtl" ? 5 : 0,
marginRight: direction === "rtl" ? 0 : 5,
color: inlineTextColor,
fontSize: markdownFontSizes.m,
lineHeight: markdownFontSizes.bodyLineHeight,
textAlign: ordered ? "right" : "center",
textAlign: ordered ? (direction === "rtl" ? "left" : "right") : "center",
}}
>
{ordered ? `${start + index}.` : "•"}
Expand Down Expand Up @@ -1204,6 +1267,37 @@ function useMarkdownStyles(
),
});

const createDirectionalMarkdownRenderers = (
inlineTextColor: string,
inlineCodeTextColor: string,
blockBackgroundColor: string,
blockTextColor: string,
copyTintColor: ColorValue,
preserveSoftBreaks: boolean,
highlightCode: boolean,
): Readonly<Record<TextDirection, CustomRenderers>> => ({
ltr: createMarkdownRenderers(
inlineTextColor,
inlineCodeTextColor,
blockBackgroundColor,
blockTextColor,
copyTintColor,
preserveSoftBreaks,
highlightCode,
"ltr",
),
rtl: createMarkdownRenderers(
inlineTextColor,
inlineCodeTextColor,
blockBackgroundColor,
blockTextColor,
copyTintColor,
preserveSoftBreaks,
highlightCode,
"rtl",
),
});

const userTheme: PartialMarkdownTheme = {
...baseTheme,
colors: {
Expand Down Expand Up @@ -1253,7 +1347,7 @@ function useMarkdownStyles(
user: {
theme: userTheme,
styles: userStyles,
renderers: createMarkdownRenderers(
renderers: createDirectionalMarkdownRenderers(
markdownUserCodeText,
markdownUserInlineCodeText,
markdownUserFenceBg,
Expand Down Expand Up @@ -1287,7 +1381,7 @@ function useMarkdownStyles(
assistant: {
theme: assistantTheme,
styles: assistantStyles,
renderers: createMarkdownRenderers(
renderers: createDirectionalMarkdownRenderers(
markdownCodeText,
markdownInlineCodeText,
markdownCodeBg,
Expand Down Expand Up @@ -1861,16 +1955,7 @@ function LegacyUserMessageContent(props: UserMessageContentProps) {
/>
);
}
return (
<Markdown
options={{ gfm: true }}
renderers={props.markdownStyles.renderers}
styles={props.markdownStyles.styles}
theme={props.markdownStyles.theme}
>
{text}
</Markdown>
);
return <NitroMarkdownMessage text={props.text} markdownStyles={props.markdownStyles} />;
}

return (
Expand Down Expand Up @@ -1903,15 +1988,11 @@ function LegacyUserMessageContent(props: UserMessageContentProps) {
renderImage={props.renderImage}
/>
) : (
<Markdown
<NitroMarkdownMessage
key={segment.id}
options={{ gfm: true }}
renderers={props.markdownStyles.renderers}
styles={props.markdownStyles.styles}
theme={props.markdownStyles.theme}
>
{text}
</Markdown>
text={text}
markdownStyles={props.markdownStyles}
/>
);
})}
</View>
Expand Down
78 changes: 78 additions & 0 deletions apps/mobile/src/lib/textDirection.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
import type { MarkdownNode } from "react-native-nitro-markdown";
import { describe, expect, it } from "vite-plus/test";

import { resolveMarkdownNodeTextDirection, resolveTextDirection } from "./textDirection";

const documentWith = (...children: MarkdownNode[]): MarkdownNode => ({
type: "document",
children,
});

describe("resolveTextDirection", () => {
it("resolves Hebrew and Arabic text as right-to-left", () => {
expect(resolveTextDirection("הודעה בעברית")).toBe("rtl");
expect(resolveTextDirection("رسالة بالعربية")).toBe("rtl");
expect(resolveTextDirection("React Server Components האם להשתמש בהם בפרויקט החדש שלנו?")).toBe(
"rtl",
);
});

it("resolves English and other left-to-right scripts as left-to-right", () => {
expect(resolveTextDirection("English message")).toBe("ltr");
expect(resolveTextDirection("日本語のメッセージ")).toBe("ltr");
});

it("ignores neutral prefixes before the first letter", () => {
expect(resolveTextDirection("👋 123... שלום")).toBe("rtl");
expect(resolveTextDirection("(123) Hello")).toBe("ltr");
});

it("defaults neutral-only content to left-to-right", () => {
expect(resolveTextDirection("👋 123...")).toBe("ltr");
});

it("uses parsed prose instead of code content", () => {
expect(
resolveMarkdownNodeTextDirection(
documentWith(
{ type: "code_block", content: "npm test" },
{
type: "paragraph",
children: [
{ type: "code_inline", content: "English inline code" },
{ type: "text", content: " שלום" },
],
},
),
),
).toBe("rtl");

expect(
resolveMarkdownNodeTextDirection(
documentWith({
type: "paragraph",
children: [
{ type: "code_inline", content: "שלום" },
{ type: "text", content: " English prose" },
],
}),
),
).toBe("ltr");
});

it("ignores synthesized GitHub alert markers", () => {
expect(
resolveMarkdownNodeTextDirection(
documentWith({
type: "blockquote",
children: [
{
type: "paragraph",
children: [{ type: "text", content: "[!NOTE] הודעת התראה בעברית." }],
},
],
}),
),
).toBe("rtl");
});
});
41 changes: 41 additions & 0 deletions apps/mobile/src/lib/textDirection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { resolveTextDirection, type TextDirection } from "@t3tools/shared/textDirection";
import type { MarkdownNode } from "react-native-nitro-markdown";

const GITHUB_ALERT_MARKER = /\[!(?:NOTE|TIP|IMPORTANT|WARNING|CAUTION)\]/giu;
const MAX_MARKDOWN_PROSE_CODE_UNITS = 17_408;

function collectMarkdownProse(
node: MarkdownNode,
chunks: string[],
remainingCodeUnits: number,
): number {
if (remainingCodeUnits === 0) return 0;
if (node.type === "code_inline" || node.type === "code_block" || node.type === "image") {
return remainingCodeUnits;
}

if (node.type === "text") {
const text = (node.content ?? "").slice(0, remainingCodeUnits).replace(GITHUB_ALERT_MARKER, "");
chunks.push(text);
return remainingCodeUnits - text.length;
}

for (const child of node.children ?? []) {
remainingCodeUnits = collectMarkdownProse(child, chunks, remainingCodeUnits);
if (remainingCodeUnits === 0) break;
}
return remainingCodeUnits;
}

function markdownProse(node: MarkdownNode): string {
const chunks: string[] = [];
collectMarkdownProse(node, chunks, MAX_MARKDOWN_PROSE_CODE_UNITS);
return chunks.join("");
}

export function resolveMarkdownNodeTextDirection(node: MarkdownNode): TextDirection {
return resolveTextDirection(markdownProse(node));
}

export { resolveTextDirection };
export type { TextDirection };
5 changes: 4 additions & 1 deletion apps/web/src/components/ChatMarkdown.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -515,7 +515,10 @@ describe("ChatMarkdown skill chips", () => {
});
const mounted = renderer!;
const labels = (label: string) =>
mounted.root.findAllByType("span").filter((node) => node.children.includes(label));
mounted.root
// The label element is a <bdi> so an RTL message cannot flip the chip's text.
.findAll((node) => node.type === "span" || node.type === "bdi")
.filter((node) => node.children.includes(label));
expect(labels("2Spec")).toHaveLength(0);

await act(async () => {
Expand Down
Loading
Loading