Skip to content

Support RTL direction in native text composers#5008

Open
austinywang wants to merge 3 commits into
mainfrom
issue-5007-rtl-text-input
Open

Support RTL direction in native text composers#5008
austinywang wants to merge 3 commits into
mainfrom
issue-5007-rtl-text-input

Conversation

@austinywang
Copy link
Copy Markdown
Contributor

@austinywang austinywang commented May 30, 2026

Summary

  • add a shared AppKit NSTextView setup for natural base writing direction and natural paragraph alignment
  • apply it to the agent prompt text box and sibling cmux-owned native composition editors
  • preserve natural writing direction when composer text is installed programmatically

Scope

This PR delivers Part 1 of #5007: cmux native prompt/message composition uses natural writing direction so Hebrew/Arabic paragraphs can resolve RTL while Latin remains LTR.

Part 2 is intentionally out of scope: the terminal pane is Ghostty's cell grid, and BiDi reordering belongs upstream in Ghostty. This PR does not attempt to patch the embedded terminal grid.

Verification

  • Not run locally per task instruction: no local tests, no reload.sh, and no xcodebuild.

View with Codesmith Autofix with Codesmith
Need help on this PR? Tag @codesmith with what you need. Autofix is disabled.


Note

Low Risk
Localized AppKit text-view configuration and call sites; no auth, data, or terminal rendering changes.

Overview
Adds NaturalTextCompositionDirection.swift, an NSTextView extension that sets natural base writing direction, alignment, default paragraph style, and typing attributes, plus applyCmuxNaturalWritingDirectionToComposedText() to re-apply those settings across existing textStorage when text is set from SwiftUI.

That setup is wired into cmux-owned native composers: command palette and feedback editors in ContentView.swift, feed inline editors in FeedPanelView.swift, and the main TextBoxInput path (TextBoxInputTextView init, clear, draft install, sync, and currentTextAttributes). The Xcode project registers the new source file.

This is Part 1 of #5007 (RTL/LTR in native composition); the embedded terminal grid is unchanged.

Reviewed by Cursor Bugbot for commit 3aa84a1. Bugbot is set up for automated code reviews on this repo. Configure here.


Summary by cubic

Enable natural RTL/LTR composition in native text composers so Hebrew/Arabic render RTL while Latin stays LTR. Adds a shared NSTextView extension and applies it across the agent prompt, feedback editor, inline feed editor, and main input.

  • New Features
    • Added NaturalTextCompositionDirection.swift with NSTextView helpers to set .natural base writing direction and alignment, update paragraph style/typing attributes, and reapply to existing text storage.
    • Applied on creation and after programmatic/binding updates in ContentView, FeedPanelView, and TextBoxInput; TextBoxInputTextView configures in init, on clear, and after setContent, and currentTextAttributes now includes the natural paragraph style so new typing inherits direction.
    • Delivers Part 1 of Support RTL / bidirectional (Hebrew, Arabic) text input and rendering #5007; terminal pane unchanged (BiDi handled upstream).

Written for commit 3aa84a1. Summary will update on new commits.

Review in cubic

Summary by CodeRabbit

  • Bug Fixes
    • Applied natural writing-direction handling to composed text across the command palette, feedback composer, feed panel, and text inputs.
    • Improved paragraph style and typing attributes so alignment and base writing direction remain consistent during editing, updates, and paste/insert operations.

Review Change Stack

@vercel
Copy link
Copy Markdown

vercel Bot commented May 30, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment May 30, 2026 5:04am
cmux-staging Building Building Preview, Comment May 30, 2026 5:04am

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 30, 2026

📝 Walkthrough

Walkthrough

This PR adds an NSTextView extension to configure and apply .natural base writing direction and paragraph alignment for composed text, then calls those helpers from command palette, feedback composer, feed inline editors, and TextBoxInput initialization/update paths.

Changes

Natural Writing Direction for Composed Text

Layer / File(s) Summary
Natural writing direction extension contract
Sources/NaturalTextCompositionDirection.swift
NSTextView extension adds configureCmuxNaturalWritingDirectionForComposedText(), applyCmuxNaturalWritingDirectionToComposedText(), and cmuxNaturalComposedTextParagraphStyle() to set .natural base writing direction/alignment, update typingAttributes, and apply paragraph style across the full text range.
Project build system integration
cmux.xcodeproj/project.pbxproj
Adds NaturalTextCompositionDirection.swift to the Xcode project (PBXBuildFile, PBXFileReference, Sources group, and PBXSourcesBuildPhase) so the new extension is compiled into the cmux target.
TextBoxInput lifecycle integration
Sources/TextBoxInput.swift
TextBoxInputTextView configures natural writing-direction at initialization, reconfigures on clear and after installing attributed content, TextBoxInputView.updateNSView reapplies the setting after syncing textView.string, and currentTextAttributes() now includes the natural paragraph style and baseline offset.
ContentView command palette and feedback editors
Sources/ContentView.swift
CommandPaletteMultilineTextEditorView and FeedbackComposerMessageEditor now call applyCmuxNaturalWritingDirectionToComposedText() during NSView creation and when updating bound text to ensure composed text uses .natural writing direction.
FeedPanelView inline text editors
Sources/Feed/FeedPanelView.swift
FeedInlineTextEditorView and FeedInlineTextField initialize and reapply the natural writing-direction behavior when creating the editor view and when programmatically syncing the editor string.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related issues

  • #5007: The change implements NSTextView helpers and applies natural writing-direction to editors, addressing the prompt/message-box natural-writing-direction concern raised in that issue.

Poem

🐰 I tune the text to flow as meant,
Natural directions softly sent,
From palette, feed, to feedback view,
Composed words find their pathway true,
Hop — and read — the lines align content.

🚥 Pre-merge checks | ✅ 16 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ⚠️ Warning The pull request description covers summary and scope well, but lacks testing details and demo materials required by the template. Add 'Testing' section describing how the change was tested locally, a 'Demo Video' section (even if noting it was not run per task instruction), and complete the checklist confirming testing and documentation updates.
✅ Passed checks (16 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Support RTL direction in native text composers' directly summarizes the main change: adding RTL support to native text composition views.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Cmux Swift Actor Isolation ✅ Passed NSTextView extensions are implicitly MainActor (AppKit UI type). No problematic value models, service protocols, shared mutable Sendables, or background context access introduced.
Cmux Swift Blocking Runtime ✅ Passed PR introduces no blocking, timing-based synchronization, sleeps, semaphores, main-queue sync, manual locks, or polling. All changes are deterministic NSTextView layout/style operations.
Cmux No Hacky Sleeps ✅ Passed PR contains only Swift source code and Xcode project configuration; check applies only to TypeScript, JavaScript, shell, and non-Swift build/runtime scripts per rule scope.
Cmux Algorithmic Complexity ✅ Passed PR adds text direction helpers for single NSTextView instances called during view creation/updates, not on scalable collections or user-owned records—matches PASS criteria.
Cmux Swift Concurrency ✅ Passed PR adds only synchronous AppKit NSTextView helpers for RTL support; no legacy async patterns (Dispatch queues, Tasks, completion handlers, Combine) are introduced.
Cmux Swift @Concurrent ✅ Passed PR adds only synchronous NSTextView helper methods for text direction. No async/await, no @concurrent annotations, no heavy operations—pure UI layout helpers comply with Swift concurrency rules.
Cmux Swift File And Package Boundaries ✅ Passed PR adds 33-line AppKit NSTextView extension with single responsibility and small integrations (+6/+3/+14 lines). Complies with allowed cases for small UI/AppKit glue.
Cmux Swift Logging ✅ Passed PR adds RTL text composition support with helper methods for NSTextView—no logging statements (print, debugPrint, dump, NSLog, Logger) added; content is app logic only.
Cmux User-Facing Error Privacy ✅ Passed PR adds internal AppKit text layout configuration only; no user-facing errors, alerts, or restricted information exposed.
Cmux Full Internationalization ✅ Passed PR adds only AppKit text layout helpers with no user-facing strings. No localization files modified or new UI text introduced—purely technical RTL/LTR direction configuration.
Cmux Swiftui State Layout ✅ Passed PR adds AppKit NSTextView configuration for RTL text direction in NSViewRepresentable bridge views; no new SwiftUI state patterns, no layout changes, no render-time mutations—fits allowed cases.
Cmux Architecture Rethink ✅ Passed PR adds NSTextView extension for natural writing direction; no timing repairs, new state, duplicate entrypoints, or UI lifecycle splits; allowed platform bridge code.
Cmux Swift Auxiliary Window Close Shortcuts ✅ Passed PR modifies only NSTextView text composition direction within existing views; does not create or materially change standalone NSWindow, NSPanel, NSWindowController, or SwiftUI Window/WindowGroup.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch issue-5007-rtl-text-input

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 561bee0. Configure here.

Comment thread Sources/TextBoxInput.swift Outdated
textView.textContainerInset = TextBoxLayout.textInset
textView.textContainer?.lineFragmentPadding = 0
textView.registerForDraggedTypes([.fileURL])
textView.configureCmuxNaturalWritingDirectionForComposedText()
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Redundant triple-configuration of writing direction on creation

Low Severity

configureCmuxNaturalWritingDirectionForComposedText() is called three times during TextBoxInputTextView creation: once in init (line 3894), once in makeNSView right after init (line 3693), and once in updateTextView called from makeNSView (line 3755). The call at line 3693 is completely redundant with the init. Additionally, the call at line 3755 in updateTextView runs on every SwiftUI update cycle and is redundant with refreshInlineAttachmentCells one line above, which already sets typingAttributes including .paragraphStyle via currentTextAttributes().

Additional Locations (2)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 561bee0. Configure here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed by relying on TextBoxInputTextView init for creation-time setup and removing the redundant make/update configure calls; refreshInlineAttachmentCells still preserves the natural paragraph style through currentTextAttributes().

— Claude Code

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 30, 2026

Greptile Summary

Adds natural writing direction (RTL/LTR auto-resolution) to all cmux-owned AppKit text composers. A new NaturalTextCompositionDirection.swift extension provides two shared helpers — configureCmuxNaturalWritingDirectionForComposedText (view-level properties) and applyCmuxNaturalWritingDirectionToComposedText (view + text-storage) — and they are wired into every affected call site.

  • New file: NaturalTextCompositionDirection.swift adds a 33-line NSTextView extension with three cmux-prefixed helpers for setting .natural alignment, base writing direction, paragraph style, and typing attributes.
  • TextBoxInputTextView: gains an explicit init override that applies direction on construction, direction is re-applied in clearContent and installAttributedContent, and .paragraphStyle is added to currentTextAttributes so newly typed characters inherit the natural paragraph style.
  • ContentView + FeedPanelView + FeedbackComposerMessageEditor: configure is called once in each view's creation path, and apply is called each time text is pushed programmatically in updateNSView.

Confidence Score: 5/5

Safe to merge — changes are confined to AppKit text layout properties with no effect on data, auth, or terminal rendering.

All five changed files touch only AppKit paragraph/writing-direction attributes on NSTextView instances. The new helpers are small, well-scoped, and correctly applied: configure is called once at view creation and apply is called only when text is programmatically mutated. The change does not touch concurrency, data persistence, or any shared mutable state.

No files require special attention.

Important Files Changed

Filename Overview
Sources/NaturalTextCompositionDirection.swift New shared NSTextView extension; logic is correct. defaultParagraphStyle?.mutableCopy() as? NSMutableParagraphStyle cast is safe, and paragraphStyle.copy() as! NSParagraphStyle force-cast is safe because NSMutableParagraphStyle.copy() always returns the immutable type.
Sources/TextBoxInput.swift Direction is applied in init, clearContent, installAttributedContent, and updateNSView text-change path. configure/apply calls are correctly scoped. Adding .paragraphStyle to currentTextAttributes ensures new typing carries natural direction. No issues with the hot update path.
Sources/ContentView.swift configure called once in makeNSView init, apply called in updateNSView only when text actually changes — correct pattern consistent with FeedbackComposerMessageEditor guidance.
Sources/Feed/FeedPanelView.swift configure in FeedInlineTextEditorView.init; apply in makeNSView after string assignment and in updateNSView's text-changed branch. Pattern is correct.
cmux.xcodeproj/project.pbxproj New file registered in both PBXBuildFile and PBXFileReference sections; added to the correct Sources phase. No issues.

Sequence Diagram

sequenceDiagram
    participant SwiftUI as SwiftUI Binding
    participant Rep as NSViewRepresentable
    participant ContainerView as Container NSView
    participant TV as NSTextView

    Note over ContainerView,TV: makeNSView / init path
    SwiftUI->>Rep: makeNSView
    Rep->>ContainerView: init
    ContainerView->>TV: configureCmuxNaturalWritingDirection()
    Rep->>TV: "string = text"
    Rep->>TV: applyCmuxNaturalWritingDirection()

    Note over SwiftUI,TV: updateNSView path (text changed)
    SwiftUI->>Rep: updateNSView (new text)
    Rep->>TV: "string = text"
    Rep->>TV: applyCmuxNaturalWritingDirection()

    Note over TV: TextBoxInputTextView internal paths
    TV->>TV: clearContent → configure
    TV->>TV: installAttributedContent → apply
Loading

Reviews (2): Last reviewed commit: "Address composer RTL review feedback" | Re-trigger Greptile

Comment on lines 3754 to 3756
textView.refreshInlineAttachmentCells(font: font, foregroundColor: foregroundColor)
textView.configureCmuxNaturalWritingDirectionForComposedText()
textView.recenterSingleLineTextContainer()
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 configureCmuxNaturalWritingDirectionForComposedText() is now called inside updateTextView, which runs on every updateNSView invocation — font changes, color refreshes, and any unrelated binding update all trigger it. Each call allocates an NSMutableParagraphStyle, copies the typingAttributes dictionary, and writes both defaultParagraphStyle and typingAttributes back. TextBoxInputTextView.init already calls this in the designated initializer, and the makeNSView setup block calls it once on creation, so direction state is established before any update cycle. Placing it here adds continuous allocation pressure on the main thread for no change in behavior. Compare FeedbackComposerMessageEditor, which correctly limits the configure call to makeNSView and only calls the apply variant when text actually changes.

Suggested change
textView.refreshInlineAttachmentCells(font: font, foregroundColor: foregroundColor)
textView.configureCmuxNaturalWritingDirectionForComposedText()
textView.recenterSingleLineTextContainer()
textView.refreshInlineAttachmentCells(font: font, foregroundColor: foregroundColor)
textView.recenterSingleLineTextContainer()

Rule Used: Flag SwiftUI changes that can cause stale state, b... (source)

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed by removing the hot-path configure call from updateNSView. The text view is configured when created, and refreshInlineAttachmentCells keeps the same natural paragraph style in typing attributes.

— Claude Code

?? NSMutableParagraphStyle()
paragraphStyle.alignment = .natural
paragraphStyle.baseWritingDirection = .natural
return paragraphStyle.copy() as? NSParagraphStyle ?? paragraphStyle
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 NSMutableParagraphStyle.copy() always returns NSParagraphStyle (the immutable type), so as? NSParagraphStyle never fails and the ?? paragraphStyle branch is dead code. The fallback would silently return the mutable copy (an NSMutableParagraphStyle upcast) in an impossible code path, which also defeats the immutability guarantee the copy was meant to provide. The trailing fallback can be removed.

Suggested change
return paragraphStyle.copy() as? NSParagraphStyle ?? paragraphStyle
return paragraphStyle.copy() as! NSParagraphStyle

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed by returning the immutable paragraph-style copy directly with the AppKit-guaranteed cast.

— Claude Code

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@Sources/NaturalTextCompositionDirection.swift`:
- Line 31: Replace the force cast on paragraphStyle.copy() with a safe cast and
fallback: call paragraphStyle.copy(), attempt to cast to NSParagraphStyle using
as?, and return the result or fall back to the original paragraphStyle;
reference the paragraphStyle variable, its copy() call, and NSParagraphStyle in
this change to satisfy SwiftLint's force_cast rule.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

Run ID: a96995f5-1181-4cb4-876f-2af97ed817af

📥 Commits

Reviewing files that changed from the base of the PR and between 561bee0 and 3aa84a1.

📒 Files selected for processing (2)
  • Sources/NaturalTextCompositionDirection.swift
  • Sources/TextBoxInput.swift
💤 Files with no reviewable changes (1)
  • Sources/TextBoxInput.swift

?? NSMutableParagraphStyle()
paragraphStyle.alignment = .natural
paragraphStyle.baseWritingDirection = .natural
return paragraphStyle.copy() as! NSParagraphStyle
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial | ⚡ Quick win

Avoid the force cast flagged by SwiftLint.

copy() returns Any; while the cast is safe here, the force cast trips the force_cast rule. Use a safe cast with a fallback (an NSMutableParagraphStyle is itself an NSParagraphStyle, so it satisfies the return type).

♻️ Proposed fix
-        return paragraphStyle.copy() as! NSParagraphStyle
+        return paragraphStyle.copy() as? NSParagraphStyle ?? paragraphStyle
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
return paragraphStyle.copy() as! NSParagraphStyle
return paragraphStyle.copy() as? NSParagraphStyle ?? paragraphStyle
🧰 Tools
🪛 SwiftLint (0.63.2)

[Error] 31-31: Force casts should be avoided

(force_cast)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@Sources/NaturalTextCompositionDirection.swift` at line 31, Replace the force
cast on paragraphStyle.copy() with a safe cast and fallback: call
paragraphStyle.copy(), attempt to cast to NSParagraphStyle using as?, and return
the result or fall back to the original paragraphStyle; reference the
paragraphStyle variable, its copy() call, and NSParagraphStyle in this change to
satisfy SwiftLint's force_cast rule.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant