From 0d66eabb102aceaf863bbb8cbd009014ad88c111 Mon Sep 17 00:00:00 2001
From: Asim M Al Twijry <3624441+AsimNet@users.noreply.github.com>
Date: Sat, 15 Aug 2026 20:27:25 +0000
Subject: [PATCH 1/8] fix(web): read Arabic and Hebrew messages in the right
direction
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
A message renders under the app's direction rather than its own, so Arabic
prose comes out with its trailing punctuation on the wrong end, inline code and
file paths displaced inside the sentence, and list bullets and quote bars on the
side opposite the text they belong to.
Each block of message markdown now carries dir="auto", so the browser takes that
block's base direction from its own first strong character — one Arabic
paragraph and one English paragraph in the same message each read correctly.
Code and tables opt out and stay left-to-right, since identifiers, paths, and
column order are not prose. Only the outermost block of a run is marked, because
dir="auto" skips descendants that carry their own dir: marking a list and its
items both would leave the list with no text to judge and paint its bullets into
a gutter that had moved. The list, quote, and task-list gutters in the
stylesheet become logical so they follow the marker.
Thread titles and project names get the same treatment: they are generated from
the user's own prompt, and truncating them needs the ellipsis on the correct end.
Written by Claude Opus 5 in Claude Code.
---
apps/web/src/components/ChatMarkdown.test.tsx | 46 +++++++++++++-
apps/web/src/components/ChatMarkdown.tsx | 63 +++++++++++++++++++
apps/web/src/components/LegacySidebar.tsx | 7 ++-
apps/web/src/components/Sidebar.tsx | 22 +++++--
apps/web/src/components/chat/ChatHeader.tsx | 18 ++++--
.../components/chat/MessagesTimeline.test.tsx | 4 +-
.../src/components/chat/MessagesTimeline.tsx | 12 +++-
apps/web/src/index.css | 15 +++--
8 files changed, 166 insertions(+), 21 deletions(-)
diff --git a/apps/web/src/components/ChatMarkdown.test.tsx b/apps/web/src/components/ChatMarkdown.test.tsx
index 9499ee5a6915..c2b4004a7b25 100644
--- a/apps/web/src/components/ChatMarkdown.test.tsx
+++ b/apps/web/src/components/ChatMarkdown.test.tsx
@@ -1,6 +1,7 @@
+import { renderToStaticMarkup } from "react-dom/server";
import { describe, expect, it } from "vite-plus/test";
-import { orderedListGutterStyle } from "./ChatMarkdown";
+import ChatMarkdown, { orderedListGutterStyle } from "./ChatMarkdown";
describe("orderedListGutterStyle", () => {
it("leaves the default gutter alone for single-digit lists", () => {
@@ -34,3 +35,46 @@ describe("orderedListGutterStyle", () => {
expect(orderedListGutterStyle(0, undefined)).toBeUndefined();
});
});
+
+describe("chat markdown text direction", () => {
+ function render(text: string) {
+ return renderToStaticMarkup(
English first.
'); + expect(html).toContain('مرحبا بالعالم.
'); + }); + + it("marks headings, lists, and quotes so their markers follow the text", () => { + const html = render("# عنوان\n\n- عنصر\n\n> اقتباس"); + expect(html).toContain(''); + }); + + it("marks only the outermost block, so a container still sees its own text", () => { + // A nested `dir` would be skipped when the browser resolves the outer + // `dir="auto"`, leaving the list LTR and its bullets in the wrong gutter. + const html = render("- عنصر\n\n> اقتباس"); + expect(html).toContain("- "); + expect(html).not.toContain("
- \n
'); + }); + + it("pins code left-to-right so an Arabic comment cannot reorder a snippet", () => { + const html = render("`git status` وأيضا\n\n```sh\n# تعليق\ngit status\n```"); + // The paragraph around it still reads right-to-left; only the code opts out. + expect(html).toContain('
'); + expect(html).toContain('
git status'); + expect(html).toContain('{ + const html = render("| اسم | value |\n| --- | --- |\n| قيمة | 1 |"); + expect(html).toContain(''); + expect(html).toContain('
'); + expect(html).toContain(' '); + }); +}); diff --git a/apps/web/src/components/ChatMarkdown.tsx b/apps/web/src/components/ChatMarkdown.tsx index c4548540e2ce..26b963b48a0e 100644 --- a/apps/web/src/components/ChatMarkdown.tsx +++ b/apps/web/src/components/ChatMarkdown.tsx @@ -195,6 +195,7 @@ const CHAT_MARKDOWN_REMARK_PLUGINS = [ remarkNormalizeListItemIndentation, remarkPreserveCodeMeta, remarkTagInlineCode, + remarkTextDirection, ] satisfies NonNullable ; const CHAT_MARKDOWN_REMARK_PLUGINS_WITH_BREAKS = [ @@ -204,6 +205,7 @@ const CHAT_MARKDOWN_REMARK_PLUGINS_WITH_BREAKS = [ remarkBreaks, remarkPreserveCodeMeta, remarkTagInlineCode, + remarkTextDirection, ] satisfies NonNullable ; const CHAT_MARKDOWN_REHYPE_PLUGINS = [ @@ -340,6 +342,64 @@ function remarkTagInlineCode() { }; } +/** + * Message prose belongs to whoever wrote it, so its direction is a property of + * the text and not of the app: `dir="auto"` makes the browser read each block's + * base direction off that block's own first strong character, which is what + * puts an Arabic sentence's trailing punctuation and its list markers on the + * right side without touching the English block above it. + * + * Code and tables opt out and stay LTR. Their shape is not prose — identifiers, + * paths, and column order read the same in every locale, and letting an Arabic + * comment flip a snippet would misreport what the agent actually wrote. + */ +const AUTO_DIRECTION_NODE_TYPES = new Set([ + "blockquote", + "heading", + "list", + "paragraph", + "tableCell", +]); +const LTR_DIRECTION_NODE_TYPES = new Set(["code", "inlineCode", "table"]); + +function setDirection(node: MarkdownAstNode, dir: "auto" | "ltr") { + node.data = { + ...node.data, + hProperties: { + ...node.data?.hProperties, + dir, + }, + }; +} + +function remarkTextDirection() { + return (tree: MarkdownAstNode) => { + // `dir="auto"` reads the first strong character of an element's *own* text + // and skips any descendant that carries its own `dir`. So only the outermost + // block of a run gets marked: marking a list and its items both would leave + // the list itself with no text to judge, fall back to LTR, and paint the + // bullets of an RTL item into a gutter that is no longer on that side. + const visit = (node: MarkdownAstNode, insideAutoBlock: boolean) => { + const type = node.type ?? ""; + if (LTR_DIRECTION_NODE_TYPES.has(type)) { + setDirection(node, "ltr"); + // A pinned table is not an `auto` ancestor, so its cells are free to + // pick their own direction while the column order stays put. + node.children?.forEach((child) => visit(child, false)); + return; + } + + const isAutoBlock = !insideAutoBlock && AUTO_DIRECTION_NODE_TYPES.has(type); + if (isAutoBlock) { + setDirection(node, "auto"); + } + node.children?.forEach((child) => visit(child, insideAutoBlock || isAutoBlock)); + }; + + visit(tree, false); + }; +} + function nodeToPlainText(node: ReactNode): string { if (typeof node === "string" || typeof node === "number") { return String(node); @@ -671,6 +731,9 @@ function MarkdownCodeBlock({ return ( @@ -737,7 +738,11 @@ export const SidebarThreadRow = memo(function SidebarThreadRow(props: SidebarThr } /> -+ {thread.title} diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index 31a73d133075..c650bf887fcf 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -290,7 +290,10 @@ function SidebarThreadTooltip({ className="max-w-80 text-left whitespace-normal [&_[data-slot=tooltip-viewport]]:p-0" >-+{thread.title}@@ -302,7 +305,9 @@ function SidebarThreadTooltip({ faviconPath={projectFaviconPath} className="size-3 shrink-0 stroke-muted-foreground" /> -) : null} {environmentLabel ? ( @@ -533,7 +538,10 @@ const SidebarDraftRow = memo(function SidebarDraftRow(props: { faviconPath={props.projectFaviconPath} className="size-4 shrink-0" /> - + {props.projectTitle} @@ -1124,6 +1132,9 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: { /> ) : ( {props.projectTitle ? ( - {thread.title} + + {thread.title} + {threadTimeLabel(thread)} diff --git a/apps/web/src/components/chat/ChatHeader.tsx b/apps/web/src/components/chat/ChatHeader.tsx index d032b16a186b..d3eea99763ad 100644 --- a/apps/web/src/components/chat/ChatHeader.tsx +++ b/apps/web/src/components/chat/ChatHeader.tsx @@ -253,7 +253,9 @@ export const ChatHeader = memo(function ChatHeader({ faviconPath={activeProjectFaviconPath} className="size-3.5" /> - {activeProjectName} + + {activeProjectName} +{projectTitle}++ {projectTitle} +New thread in {activeProjectName} @@ -289,24 +291,30 @@ export const ChatHeader = memo(function ChatHeader({ /> } > -{activeThreadTitle}
++ {activeThreadTitle} +
- {activeThreadTitle} ++ {activeThreadTitle} + ) : ()} diff --git a/apps/web/src/components/chat/MessagesTimeline.test.tsx b/apps/web/src/components/chat/MessagesTimeline.test.tsx index 617ee0b80d1c..1945a605547d 100644 --- a/apps/web/src/components/chat/MessagesTimeline.test.tsx +++ b/apps/web/src/components/chat/MessagesTimeline.test.tsx @@ -520,7 +520,9 @@ describe("MessagesTimeline", () => { />, ); - expect(markup).toContain(' + {activeThreadTitle}
} /> -{activeThreadTitle} ++ {activeThreadTitle} + <tag attr="x">'); + expect(markup).toContain( + '<tag attr="x">', + ); expect(markup).toContain("<root><child enabled="true" /></root>"); }); diff --git a/apps/web/src/components/chat/MessagesTimeline.tsx b/apps/web/src/components/chat/MessagesTimeline.tsx index c90aa771f8d1..9ff416f40225 100644 --- a/apps/web/src/components/chat/MessagesTimeline.tsx +++ b/apps/web/src/components/chat/MessagesTimeline.tsx @@ -1773,7 +1773,10 @@ const UserMessageBody = memo(function UserMessageBody(props: { } return ( -+{comment.text.length > 0 && ( -{inlineNodes}); @@ -1812,7 +1815,10 @@ const UserMessageBody = memo(function UserMessageBody(props: { } return ( -+{inlineNodes}); @@ -1854,7 +1860,7 @@ function UserMessageReviewCommentCard({ comment }: { comment: ReviewCommentConte+)} diff --git a/apps/web/src/index.css b/apps/web/src/index.css index fea03489b7fa..c1758876c5fe 100644 --- a/apps/web/src/index.css +++ b/apps/web/src/index.css @@ -1640,7 +1640,9 @@ code { custom property, so without this a task-list under a 3+ digit ordered list would inherit the outer gutter instead of its own default. */ --list-gutter: 1.25rem; - padding-left: 1.25rem; + /* Logical, because a list whose text is Arabic or Hebrew carries dir="auto" + and paints its markers on the right — the gutter has to move with them. */ + padding-inline-start: 1.25rem; list-style-type: disc; } @@ -1651,7 +1653,7 @@ code { nested ol without its own widened marker doesn't inherit the outer one. */ .chat-markdown ol { --list-gutter: 1.25rem; - padding-left: var(--list-gutter, 1.25rem); + padding-inline-start: var(--list-gutter, 1.25rem); list-style-type: decimal; } @@ -1681,7 +1683,8 @@ code { } .chat-markdown li.task-list-item input[type="checkbox"] { - margin: 0 0.35em 0.15em calc(-1 * var(--list-gutter, 1.25rem)); + margin: 0 0 0.15em; + margin-inline: calc(-1 * var(--list-gutter, 1.25rem)) 0.35em; vertical-align: middle; } @@ -1705,8 +1708,8 @@ code { } .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); } @@ -1720,7 +1723,7 @@ code { .chat-markdown section[data-footnotes] ol { margin: 0; - padding-left: 1.25rem; + padding-inline-start: 1.25rem; } .chat-markdown section[data-footnotes] li + li { From 8d90e3abf69ef5bbf365a444f2321414ff8491bd Mon Sep 17 00:00:00 2001 From: Asim M Al Twijry <3624441+AsimNet@users.noreply.github.com> Date: Sat, 15 Aug 2026 21:22:56 +0000 Subject: [PATCH 2/8] fix(web): carry direction into alerts, file chips, and rename inputs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review follow-ups on the same concern. A GitHub alert is not rendered as a blockquote — its renderer builds a titled callout from scratch — so claiming the blockquote as the marked block stranded the body: the dir never reached the callout, and the paragraphs inside it were skipped as already-covered. Alert blockquotes are no longer claimed, so their paragraphs carry their own direction under LTR chrome, and that chrome's gutter becomes logical. A file path is an identifier, but the code renderer swaps a chip in for the `` it replaces, so the pin was lost exactly where the PR claimed to fix it. The chip carries it now. The terminal-context wrapper drops its dir: the chips always precede the message text, so it could only ever resolve from a chip label, while the markdown below already picks its own direction per block. Thread-title rename inputs get dir="auto" so a title does not flip direction the moment it is edited. Written by Claude Opus 5 in Claude Code. --- apps/web/src/components/ChatMarkdown.test.tsx | 15 +++++++++++++++ apps/web/src/components/ChatMarkdown.tsx | 18 ++++++++++++++++-- apps/web/src/components/LegacySidebar.tsx | 1 + apps/web/src/components/Sidebar.tsx | 1 + .../src/components/chat/MessagesTimeline.tsx | 8 ++++---- 5 files changed, 37 insertions(+), 6 deletions(-) diff --git a/apps/web/src/components/ChatMarkdown.test.tsx b/apps/web/src/components/ChatMarkdown.test.tsx index c2b4004a7b25..c5cf63efeecf 100644 --- a/apps/web/src/components/ChatMarkdown.test.tsx +++ b/apps/web/src/components/ChatMarkdown.test.tsx @@ -71,6 +71,21 @@ describe("chat markdown text direction", () => { expect(html).toContain('{ + // The alert renderer builds its own element, so the blockquote cannot be the + // marked block — the body paragraphs have to carry the direction instead. + const html = render("> [!NOTE]\n> مرحبا بالعالم."); + expect(html).toContain('مرحبا بالعالم.
'); + expect(html).not.toContain("{ + // The `code` renderer swaps the chip in for the `` it + // replaces, so a path in an Arabic sentence keeps its own reading order. + const html = render("عدّل `src/main.ts` من فضلك."); + expect(html).toContain(' { const html = render("| اسم | value |\n| --- | --- |\n| قيمة | 1 |"); expect(html).toContain(''); diff --git a/apps/web/src/components/ChatMarkdown.tsx b/apps/web/src/components/ChatMarkdown.tsx index 26b963b48a0e..8cd688647111 100644 --- a/apps/web/src/components/ChatMarkdown.tsx +++ b/apps/web/src/components/ChatMarkdown.tsx @@ -379,6 +379,10 @@ function remarkTextDirection() { // block of a run gets marked: marking a list and its items both would leave // the list itself with no text to judge, fall back to LTR, and paint the // bullets of an RTL item into a gutter that is no longer on that side. + // + // The cost is that one list reads in one direction. A list that mixes an + // Arabic item with an English one takes the direction of its first item, + // which is the trade for markers that stay next to the text they label. const visit = (node: MarkdownAstNode, insideAutoBlock: boolean) => { const type = node.type ?? ""; if (LTR_DIRECTION_NODE_TYPES.has(type)) { @@ -389,7 +393,13 @@ function remarkTextDirection() { return; } - const isAutoBlock = !insideAutoBlock && AUTO_DIRECTION_NODE_TYPES.has(type); + // A GitHub alert is rendered as a titled callout rather than a quote, and + // its own renderer builds that chrome from scratch. Claiming the block + // here would strand its body: the `dir` never reaches the callout, and the + // paragraphs inside it would have been skipped as already-covered. + const isAlertBlockquote = type === "blockquote" && node.data?.hProperties?.dataAlert != null; + const isAutoBlock = + !insideAutoBlock && !isAlertBlockquote && AUTO_DIRECTION_NODE_TYPES.has(type); if (isAutoBlock) { setDirection(node, "auto"); } @@ -1364,6 +1374,10 @@ const MarkdownFileLink = memo(function MarkdownFileLink({
` it replaces, so the pin has to live here too. + dir="ltr" href={href} className={cn(CHAT_FILE_TAG_CHIP_CLASS_NAME, MARKDOWN_FILE_LINK_CLASS_NAME, className)} data-markdown-copy={copyMarkdown} @@ -1631,7 +1645,7 @@ function ChatMarkdown({ // Not a : the stylesheet mutes those, and an alert's body is ordinary // text under a colored title — which is how the host renders it. return ( -+From c430d4b32f7d0c600c3d5e6718e615894bb8436f Mon Sep 17 00:00:00 2001 From: Asim M Al Twijry <3624441+AsimNet@users.noreply.github.com> Date: Sat, 15 Aug 2026 22:00:35 +0000 Subject: [PATCH 6/8] fix(web): narrow the palette to the one string that is message text MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backing out my own overreach. The palette's title span is shared with the file picker and the add-project browser, where the title is a basename or directory name — an identifier, which this PR otherwise pins left-to-right. Marking it auto could reorder an Arabic filename against its Latin extension, so those titles go back to inheriting the app's direction. The search excerpt keeps the treatment. It is a message quoted back to the user, which is what this PR is about, and it needs its own wrapper because the "You:"/"Agent:" label in front of it fixes the line itself left-to-right. Written by Claude Opus 5 in Claude Code. --- .../src/components/CommandPaletteResults.tsx | 20 +++++++------------ 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/apps/web/src/components/CommandPaletteResults.tsx b/apps/web/src/components/CommandPaletteResults.tsx index 69f8e4d0e0fc..97a2b9856997 100644 --- a/apps/web/src/components/CommandPaletteResults.tsx +++ b/apps/web/src/components/CommandPaletteResults.tsx @@ -74,7 +74,9 @@ function ThreadContentMatch(props: { {isUser ? "You:" : "Agent:"} {" "} -
{alert.label} diff --git a/apps/web/src/components/LegacySidebar.tsx b/apps/web/src/components/LegacySidebar.tsx index 5eb7b0146b8f..aeca799540ed 100644 --- a/apps/web/src/components/LegacySidebar.tsx +++ b/apps/web/src/components/LegacySidebar.tsx @@ -717,6 +717,7 @@ export const SidebarThreadRow = memo(function SidebarThreadRow(props: SidebarThr {renamingThreadKey === threadKey ? ( onRenameTitleChange(event.target.value)} diff --git a/apps/web/src/components/chat/MessagesTimeline.tsx b/apps/web/src/components/chat/MessagesTimeline.tsx index 9ff416f40225..0ed366de4a20 100644 --- a/apps/web/src/components/chat/MessagesTimeline.tsx +++ b/apps/web/src/components/chat/MessagesTimeline.tsx @@ -1815,10 +1815,10 @@ const UserMessageBody = memo(function UserMessageBody(props: { } return ( - + // No `dir` here: the terminal-context chips always come first, so the + // wrapper could only ever resolve from their label. The message text below + // them is markdown, and each of its blocks picks its own direction. +-{inlineNodes}); From adcffb402a9198071f807a14af49597ac8812dc7 Mon Sep 17 00:00:00 2001 From: Asim M Al Twijry <3624441+AsimNet@users.noreply.github.com> Date: Sat, 15 Aug 2026 21:36:21 +0000 Subject: [PATCH 3/8] fix(web): align table cells to their own text The last physical inline property in the markdown stylesheet. Cells carry dir="auto" now, so an Arabic cell resolves right-to-left for ordering while `text-align: left` still pinned it to the cell's left edge. `start` follows the cell's own direction and is identical for left-to-right content; the table stays pinned so the columns keep their source order. Written by Claude Opus 5 in Claude Code. --- apps/web/src/index.css | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/apps/web/src/index.css b/apps/web/src/index.css index c1758876c5fe..59b720af8313 100644 --- a/apps/web/src/index.css +++ b/apps/web/src/index.css @@ -1835,7 +1835,9 @@ code { .chat-markdown th, .chat-markdown td { padding: 0.45rem 0.75rem; - text-align: left; + /* Logical: the table is pinned LTR so its columns keep their source order, + but each cell carries dir="auto" and aligns to its own text. */ + text-align: start; } .chat-markdown thead th { From ca9169cab12826709783e665bd32f680d8e34603 Mon Sep 17 00:00:00 2001 From: Asim M Al Twijry <3624441+AsimNet@users.noreply.github.com> Date: Sat, 15 Aug 2026 21:42:11 +0000 Subject: [PATCH 4/8] fix(web): keep the header rename input on the title's own direction The third of three rename inputs for the same title. The sidebar rows got dir="auto" but the header did not, so starting a rename there was the one place an Arabic title flipped to left-to-right under the caret and back again on commit. Written by Claude Opus 5 in Claude Code. --- apps/web/src/components/chat/ChatHeader.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/web/src/components/chat/ChatHeader.tsx b/apps/web/src/components/chat/ChatHeader.tsx index d3eea99763ad..38008344477e 100644 --- a/apps/web/src/components/chat/ChatHeader.tsx +++ b/apps/web/src/components/chat/ChatHeader.tsx @@ -267,6 +267,7 @@ export const ChatHeader = memo(function ChatHeader({ {renamingTitle !== null ? ( Date: Sat, 15 Aug 2026 21:54:50 +0000 Subject: [PATCH 5/8] fix(web): finish the truncated-text sweep for draft previews and the palette MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The draft row's second line is the prompt the user actually typed, so it needed the treatment its own project label had already received. Swept the rest of the class rather than wait to be told again: the command palette lists the same thread and project titles, and it is the other entry point to them. Command names and file paths are unaffected — they are already left-to-right, and dir="auto" resolves them the same way. Written by Claude Opus 5 in Claude Code. --- .../web/src/components/CommandPaletteResults.tsx | 16 ++++++++++++---- apps/web/src/components/Sidebar.tsx | 4 +++- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/apps/web/src/components/CommandPaletteResults.tsx b/apps/web/src/components/CommandPaletteResults.tsx index bbdbc28b0609..69f8e4d0e0fc 100644 --- a/apps/web/src/components/CommandPaletteResults.tsx +++ b/apps/web/src/components/CommandPaletteResults.tsx @@ -136,7 +136,9 @@ function DisabledCommandPaletteResultRow(props: { {props.item.titleLeadingContent} - {props.item.title} + + {props.item.title} + {props.item.threadContentMatch ? (@@ -150,7 +152,9 @@ function DisabledCommandPaletteResultRow(props: { ) : ( {props.item.titleLeadingContent} - {props.item.title} + + {props.item.title} + )} {props.item.titleTrailingContent} @@ -187,7 +191,9 @@ function CommandPaletteResultRow(props: { {props.item.titleLeadingContent} - {props.item.title} + + {props.item.title} + {props.item.threadContentMatch ? ( @@ -201,7 +207,9 @@ function CommandPaletteResultRow(props: { ) : ( {props.item.titleLeadingContent} - {props.item.title} + + {props.item.title} + )} {props.item.titleTrailingContent} diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index e7160df8d075..f08e1bade7c5 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -562,7 +562,9 @@ const SidebarDraftRow = memo(function SidebarDraftRow(props: { {preview}++ {preview} ++ + + ); } @@ -136,9 +138,7 @@ function DisabledCommandPaletteResultRow(props: { {props.item.titleLeadingContent} - - {props.item.title} - + {props.item.title} {props.item.threadContentMatch ? ( @@ -152,9 +152,7 @@ function DisabledCommandPaletteResultRow(props: { ) : ( {props.item.titleLeadingContent} - - {props.item.title} - + {props.item.title} )} {props.item.titleTrailingContent} @@ -191,9 +189,7 @@ function CommandPaletteResultRow(props: { {props.item.titleLeadingContent} - - {props.item.title} - + {props.item.title} {props.item.threadContentMatch ? ( @@ -207,9 +203,7 @@ function CommandPaletteResultRow(props: { ) : ( {props.item.titleLeadingContent} - - {props.item.title} - + {props.item.title} )} {props.item.titleTrailingContent} From 0129d2c3964d1724910f8e62d14551199cbfe46f Mon Sep 17 00:00:00 2001 From: Asim M Al Twijry <3624441+AsimNet@users.noreply.github.com> Date: Sat, 15 Aug 2026 22:10:32 +0000 Subject: [PATCH 7/8] fix(web): let a palette item say whether its title is prose MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Direction here is a property of the item, not of the row. The thread and project builders know they are emitting a title derived from someone's prompt; the file picker and the browse rows know they are emitting a basename. So the builders carry the decision, and the truncating span applies it — that span has to be the one, since it owns the ellipsis. This restores direction-aware thread and project titles in the palette, which the previous commit dropped to protect filenames, without marking identifiers. Written by Claude Opus 5 in Claude Code. --- apps/web/src/components/CommandPalette.logic.ts | 8 ++++++++ .../web/src/components/CommandPaletteResults.tsx | 16 ++++++++++++---- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/apps/web/src/components/CommandPalette.logic.ts b/apps/web/src/components/CommandPalette.logic.ts index 1fddb4f92f4a..37c629ce47da 100644 --- a/apps/web/src/components/CommandPalette.logic.ts +++ b/apps/web/src/components/CommandPalette.logic.ts @@ -89,6 +89,12 @@ export interface CommandPaletteItem { readonly value: string; readonly searchTerms: ReadonlyArray ; readonly title: ReactNode; + /** + * `"auto"` for titles that are prose the user or an agent wrote, so they read + * in their own direction. Left unset for the rest: this list also holds + * command names and file paths, and an identifier keeps the app's direction. + */ + readonly titleDir?: "auto"; readonly description?: ReactNode; readonly threadContentMatch?: CommandPaletteThreadContentMatch; readonly timestamp?: string; @@ -158,6 +164,7 @@ export function buildProjectActionItems(input: { value: `${input.valuePrefix}:${project.environmentId}:${project.id}`, searchTerms: [project.title, project.workspaceRoot, ...(input.searchTerms?.(project) ?? [])], title: project.title, + titleDir: "auto", description: input.renderDescription?.(project) ?? project.workspaceRoot, icon: input.icon(project), ...(input.shortcutCommand !== undefined ? { shortcutCommand: input.shortcutCommand } : {}), @@ -239,6 +246,7 @@ export function buildThreadActionItems {props.item.titleLeadingContent} - {props.item.title} + + {props.item.title} + {props.item.threadContentMatch ? ( @@ -152,7 +154,9 @@ function DisabledCommandPaletteResultRow(props: { ) : ( {props.item.titleLeadingContent} - {props.item.title} + + {props.item.title} + )} {props.item.titleTrailingContent} @@ -189,7 +193,9 @@ function CommandPaletteResultRow(props: { {props.item.titleLeadingContent} - {props.item.title} + + {props.item.title} + {props.item.threadContentMatch ? ( @@ -203,7 +209,9 @@ function CommandPaletteResultRow(props: { ) : ( {props.item.titleLeadingContent} - {props.item.title} + + {props.item.title} + )} {props.item.titleTrailingContent} From d8e825ac5ec5f404c2ff89f07a69bf732d11359c Mon Sep 17 00:00:00 2001 From: Asim M Al Twijry <3624441+AsimNet@users.noreply.github.com> Date: Sat, 15 Aug 2026 22:19:49 +0000 Subject: [PATCH 8/8] fix(web): let the sidebar thread tooltip align to its own text MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The popup pinned text-align to the physical left, which inherited into the title and project name it stacks — both of which resolve their own direction now, so an Arabic title read right-to-left while sitting against the left edge. The two sibling tooltips set no alignment at all and were already correct. Logical alignment renders identically for the left-to-right metadata rows. Written by Claude Opus 5 in Claude Code. --- apps/web/src/components/Sidebar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/components/Sidebar.tsx b/apps/web/src/components/Sidebar.tsx index f08e1bade7c5..a3c9ff29af82 100644 --- a/apps/web/src/components/Sidebar.tsx +++ b/apps/web/src/components/Sidebar.tsx @@ -287,7 +287,7 @@ function SidebarThreadTooltip({ align="start" sideOffset={4} variant="glass" - className="max-w-80 text-left whitespace-normal [&_[data-slot=tooltip-viewport]]:p-0" + className="max-w-80 text-start whitespace-normal [&_[data-slot=tooltip-viewport]]:p-0" >