fix(swift-ios): enable granular text selection - #6110

Merged
t3dotgg merged 3 commits into
pingdotgg:t3code/rebuild-mobile-app-swiftfrom
saphid:saphid/swiftui-text-selection
Aug 15, 2026
Merged

fix(swift-ios): enable granular text selection#6110
t3dotgg merged 3 commits into
pingdotgg:t3code/rebuild-mobile-app-swiftfrom
saphid:saphid/swiftui-text-selection

Conversation

@saphid

@saphidsaphid commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

What Changed

  • render Markdown paragraphs, headings, lists, tables, and code blocks with selectable native text
  • preserve bold, italic, inline-code, strike-through, link, and Dynamic Type styling
  • keep selections stable while a streaming response appends text
  • retain code-block wrapping/copy controls and route link actions through SwiftUI openURL
  • add focused coverage for formatting, Dynamic Type, code reuse, and selection restoration

Why

The native SwiftUI thread view only offered whole-message copying. People could not select a word, sentence, or code fragment with standard iOS selection handles.

This adds granular native selection while keeping message-level copy actions, streaming updates, accessibility actions, safe link routing, and the existing Markdown layout behavior.

Closes#6089.

UI Changes

Long-pressing rendered assistant prose or code now shows the native iOS selection handles and edit menu (Copy, Look Up, Translate). The appearance of messages is otherwise unchanged.

Verified in the iOS Simulator inside a live thread: native selection appeared on prose and the existing code-block controls remained available. A physical-phone acceptance pass will be attached after the approved-based combined build is installed.

Verification

  • MarkdownDocumentTests: 15 passed, 0 failed on iOS Simulator
  • combined simulator pass with PR fix(swift-ios): restore thread scrolling and back swipe #6090: native selection menu appeared and full-surface back swipe returned to the thread list
  • git diff --check: passed
  • fresh direct Claude Opus 5 high review of the final diff: exit 0, no actionable findings

Checklist

  • This PR is focused on one native behavior
  • I explained what changed and why
  • Before/after screenshots are not applicable because message appearance is unchanged
  • I included a video for the interaction change

Implemented with GPT-5.6 Sol in T3 Code/Codex. Independently reviewed by Claude Opus 5 high.

Note

Enable granular text selection in markdown messages using UITextView

  • Replaces SwiftUI Text-based inline rendering with a UIViewRepresentable wrapping UITextView (MarkdownInlineText), enabling native per-word/character text selection without a toggle.
  • Adds MarkdownSelectableTextAttributes to centrally build NSAttributedString from inline markdown runs, applying dynamic type, link handling, inline code monospacing, and strikethrough.
  • Adds MarkdownSelectionRestoration to preserve the user's selection range across streaming updates when the new text safely extends the previous text.
  • Propagates selectionContext and textColor through MarkdownBlocksView, MarkdownBlockView, MarkdownTableView, MarkdownListView, and MarkdownCodeBlockView so all message content is selectable.
  • Adds a configurable copyActionTitle to MarkdownMessageView, used in both the accessibility action and the UITextView edit menu; file previews use "Copy file contents".
  • Behavioral Change: the old MarkdownTextSelectionModifier toggle is removed; text selection is always active via UIKit.

Macroscope summarized 2ae671d.


Note

Medium Risk
Large UIKit bridge in the hot-path transcript with streaming selection and layout sizing; behavior changes for table cell accessibility and keyboard dismissal on scroll.

Overview
Chat markdown now renders inline content through UITextView-backed views instead of SwiftUI Text, so users get native selection handles and the system edit menu across paragraphs, headings, lists, tables, and code bodies.

A MarkdownSelectionContext carries the full message (or file) text and a configurable copy title into each text view; the delegate adds Copy message / Copy file contents to the selection menu and keeps message-level copy via accessibility actions. MarkdownSelectionRestoration preserves the selected range while streaming only when new text extends the previous string.

Rendering moves inline styling to MarkdownSelectableTextAttributes and MarkdownInlineStyle.uiFont (Dynamic Type), adds shared UIKit theme colors for text views, and pre-renders code blocks as literal .code inline runs (reused from cache). The old context-menu “Select text” toggle is removed.

Transcript scrolling no longer calls endEditing on drag; keyboard dismissal goes through the existing onDismissKeyboard path. Tests cover formatting, Dynamic Type, code reuse, and selection restoration; home metadata tests switch to providersByEnvironment fixtures.

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

Delivery: direct
Validated against Theo commit: f98cab5
Depends on: none
Merge order: this PR only
Validation status: Mergeable; native/repository checks and current review are green. The unrelated Vercel authorization failure is a maintainer-side gate.

@coderabbitai

coderabbitaiBot commented Aug 11, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 23359412-8d23-42e6-b18b-d793f74c16fb

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

@github-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XL 500-999 changed lines (additions + deletions). labels Aug 11, 2026
@saphid
saphid marked this pull request as ready for review August 11, 2026 10:53
@saphid

Copy link
Copy Markdown
ContributorAuthor

Phone acceptance update: Alex confirmed that text selection is working on the physical phone. This PR is ready for maintainer review. The stale provider-catalog native test fixture is fixed in #6130, and this branch carries the same focused test repair; the current SwiftUI native check is green.

@cursorcursorBot left a comment

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.

Cursor Bugbot has reviewed your changes using high effort 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 2ae671d. Configure here.

block: blocks[index],
selectionContext: selectionContext,
textColor: textColor
)

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.

Dynamic Type updates get skipped

Medium Severity

MarkdownInlineText now bakes fonts from @Environment(\.dynamicTypeSize), but its parent MarkdownBlockView still uses .equatable() and only compares block, selectionContext, and textColor. When Dynamic Type changes alone, equality stays true, so body is skipped and selectable text keeps the old size. This is a regression from the previous Text + text-style Font path, which scaled without an environment read. File previews have no cell reconfigure workaround.

Additional Locations (2)
Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit 2ae671d. Configure here.

@macroscopeapp

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

This PR introduces a new granular text selection feature with significant new UIKit bridging code and selection state management (~700 lines). An unresolved review comment also identifies a potential Dynamic Type accessibility regression. Human review is recommended for this new capability.

You can customize Macroscope's approvability policy. Learn more.

@t3-codet3-codeBot left a comment

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.

one blocking Dynamic Type regression remains.

selectionContext: selectionContext,
textColor: textColor
)
.equatable()

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.

MarkdownInlineText now derives UIKit fonts from dynamicTypeSize, but this equatable boundary does not include that environment value. a Dynamic Type-only change can skip rebuilding unchanged blocks, leaving selectable text at the old size. include the size/category in the equatable inputs or remove this equality optimization for these blocks.

t3-code[bot]
t3-codeBot approved these changes Aug 12, 2026

@t3-codet3-codeBot left a comment

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.

correction after independent review: the prior Dynamic Type concern is not blocking. the UIKit representable directly reads dynamicTypeSize, includes it in its attributed-text cache key, and has focused scaling coverage. selection remains block-scoped and the many-UITextView layout cost is worth monitoring, but neither is a correctness blocker for this PR. approved.

@t3dotgg
t3dotgg merged commit 50bd5cb into pingdotgg:t3code/rebuild-mobile-app-swiftAug 15, 2026
22 of 23 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XL500-999 changed lines (additions + deletions).vouch:trustedPR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@saphid@t3dotgg
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

fix(swift-ios): enable granular text selection - #6110

Merged
t3dotgg merged 3 commits into
pingdotgg:t3code/rebuild-mobile-app-swiftfrom
saphid:saphid/swiftui-text-selection
Aug 15, 2026
Merged

fix(swift-ios): enable granular text selection#6110
t3dotgg merged 3 commits into
pingdotgg:t3code/rebuild-mobile-app-swiftfrom
saphid:saphid/swiftui-text-selection

Conversation

@saphid

@saphidsaphid commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

What Changed

  • render Markdown paragraphs, headings, lists, tables, and code blocks with selectable native text
  • preserve bold, italic, inline-code, strike-through, link, and Dynamic Type styling
  • keep selections stable while a streaming response appends text
  • retain code-block wrapping/copy controls and route link actions through SwiftUI openURL
  • add focused coverage for formatting, Dynamic Type, code reuse, and selection restoration

Why

The native SwiftUI thread view only offered whole-message copying. People could not select a word, sentence, or code fragment with standard iOS selection handles.

This adds granular native selection while keeping message-level copy actions, streaming updates, accessibility actions, safe link routing, and the existing Markdown layout behavior.

Closes#6089.

UI Changes

Long-pressing rendered assistant prose or code now shows the native iOS selection handles and edit menu (Copy, Look Up, Translate). The appearance of messages is otherwise unchanged.

Verified in the iOS Simulator inside a live thread: native selection appeared on prose and the existing code-block controls remained available. A physical-phone acceptance pass will be attached after the approved-based combined build is installed.

Verification

  • MarkdownDocumentTests: 15 passed, 0 failed on iOS Simulator
  • combined simulator pass with PR fix(swift-ios): restore thread scrolling and back swipe #6090: native selection menu appeared and full-surface back swipe returned to the thread list
  • git diff --check: passed
  • fresh direct Claude Opus 5 high review of the final diff: exit 0, no actionable findings

Checklist

  • This PR is focused on one native behavior
  • I explained what changed and why
  • Before/after screenshots are not applicable because message appearance is unchanged
  • I included a video for the interaction change

Implemented with GPT-5.6 Sol in T3 Code/Codex. Independently reviewed by Claude Opus 5 high.

Note

Enable granular text selection in markdown messages using UITextView

  • Replaces SwiftUI Text-based inline rendering with a UIViewRepresentable wrapping UITextView (MarkdownInlineText), enabling native per-word/character text selection without a toggle.
  • Adds MarkdownSelectableTextAttributes to centrally build NSAttributedString from inline markdown runs, applying dynamic type, link handling, inline code monospacing, and strikethrough.
  • Adds MarkdownSelectionRestoration to preserve the user's selection range across streaming updates when the new text safely extends the previous text.
  • Propagates selectionContext and textColor through MarkdownBlocksView, MarkdownBlockView, MarkdownTableView, MarkdownListView, and MarkdownCodeBlockView so all message content is selectable.
  • Adds a configurable copyActionTitle to MarkdownMessageView, used in both the accessibility action and the UITextView edit menu; file previews use "Copy file contents".
  • Behavioral Change: the old MarkdownTextSelectionModifier toggle is removed; text selection is always active via UIKit.

Macroscope summarized 2ae671d.


Note

Medium Risk
Large UIKit bridge in the hot-path transcript with streaming selection and layout sizing; behavior changes for table cell accessibility and keyboard dismissal on scroll.

Overview
Chat markdown now renders inline content through UITextView-backed views instead of SwiftUI Text, so users get native selection handles and the system edit menu across paragraphs, headings, lists, tables, and code bodies.

A MarkdownSelectionContext carries the full message (or file) text and a configurable copy title into each text view; the delegate adds Copy message / Copy file contents to the selection menu and keeps message-level copy via accessibility actions. MarkdownSelectionRestoration preserves the selected range while streaming only when new text extends the previous string.

Rendering moves inline styling to MarkdownSelectableTextAttributes and MarkdownInlineStyle.uiFont (Dynamic Type), adds shared UIKit theme colors for text views, and pre-renders code blocks as literal .code inline runs (reused from cache). The old context-menu “Select text” toggle is removed.

Transcript scrolling no longer calls endEditing on drag; keyboard dismissal goes through the existing onDismissKeyboard path. Tests cover formatting, Dynamic Type, code reuse, and selection restoration; home metadata tests switch to providersByEnvironment fixtures.

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

Delivery: direct
Validated against Theo commit: f98cab5
Depends on: none
Merge order: this PR only
Validation status: Mergeable; native/repository checks and current review are green. The unrelated Vercel authorization failure is a maintainer-side gate.

@coderabbitai

coderabbitaiBot commented Aug 11, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 23359412-8d23-42e6-b18b-d793f74c16fb

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

@github-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XL 500-999 changed lines (additions + deletions). labels Aug 11, 2026
@saphid
saphid marked this pull request as ready for review August 11, 2026 10:53
@saphid

Copy link
Copy Markdown
ContributorAuthor

Phone acceptance update: Alex confirmed that text selection is working on the physical phone. This PR is ready for maintainer review. The stale provider-catalog native test fixture is fixed in #6130, and this branch carries the same focused test repair; the current SwiftUI native check is green.

@cursorcursorBot left a comment

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.

Cursor Bugbot has reviewed your changes using high effort 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 2ae671d. Configure here.

block: blocks[index],
selectionContext: selectionContext,
textColor: textColor
)

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.

Dynamic Type updates get skipped

Medium Severity

MarkdownInlineText now bakes fonts from @Environment(\.dynamicTypeSize), but its parent MarkdownBlockView still uses .equatable() and only compares block, selectionContext, and textColor. When Dynamic Type changes alone, equality stays true, so body is skipped and selectable text keeps the old size. This is a regression from the previous Text + text-style Font path, which scaled without an environment read. File previews have no cell reconfigure workaround.

Additional Locations (2)
Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit 2ae671d. Configure here.

@macroscopeapp

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

This PR introduces a new granular text selection feature with significant new UIKit bridging code and selection state management (~700 lines). An unresolved review comment also identifies a potential Dynamic Type accessibility regression. Human review is recommended for this new capability.

You can customize Macroscope's approvability policy. Learn more.

@t3-codet3-codeBot left a comment

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.

one blocking Dynamic Type regression remains.

selectionContext: selectionContext,
textColor: textColor
)
.equatable()

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.

MarkdownInlineText now derives UIKit fonts from dynamicTypeSize, but this equatable boundary does not include that environment value. a Dynamic Type-only change can skip rebuilding unchanged blocks, leaving selectable text at the old size. include the size/category in the equatable inputs or remove this equality optimization for these blocks.

t3-code[bot]
t3-codeBot approved these changes Aug 12, 2026

@t3-codet3-codeBot left a comment

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.

correction after independent review: the prior Dynamic Type concern is not blocking. the UIKit representable directly reads dynamicTypeSize, includes it in its attributed-text cache key, and has focused scaling coverage. selection remains block-scoped and the many-UITextView layout cost is worth monitoring, but neither is a correctness blocker for this PR. approved.

@t3dotgg
t3dotgg merged commit 50bd5cb into pingdotgg:t3code/rebuild-mobile-app-swiftAug 15, 2026
22 of 23 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XL500-999 changed lines (additions + deletions).vouch:trustedPR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@saphid@t3dotgg
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(swift-ios): enable granular text selection - #6110

Merged
t3dotgg merged 3 commits into
pingdotgg:t3code/rebuild-mobile-app-swiftfrom
saphid:saphid/swiftui-text-selection
Aug 15, 2026
Merged

fix(swift-ios): enable granular text selection#6110
t3dotgg merged 3 commits into
pingdotgg:t3code/rebuild-mobile-app-swiftfrom
saphid:saphid/swiftui-text-selection

Conversation

@saphid

@saphidsaphid commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

What Changed

  • render Markdown paragraphs, headings, lists, tables, and code blocks with selectable native text
  • preserve bold, italic, inline-code, strike-through, link, and Dynamic Type styling
  • keep selections stable while a streaming response appends text
  • retain code-block wrapping/copy controls and route link actions through SwiftUI openURL
  • add focused coverage for formatting, Dynamic Type, code reuse, and selection restoration

Why

The native SwiftUI thread view only offered whole-message copying. People could not select a word, sentence, or code fragment with standard iOS selection handles.

This adds granular native selection while keeping message-level copy actions, streaming updates, accessibility actions, safe link routing, and the existing Markdown layout behavior.

Closes#6089.

UI Changes

Long-pressing rendered assistant prose or code now shows the native iOS selection handles and edit menu (Copy, Look Up, Translate). The appearance of messages is otherwise unchanged.

Verified in the iOS Simulator inside a live thread: native selection appeared on prose and the existing code-block controls remained available. A physical-phone acceptance pass will be attached after the approved-based combined build is installed.

Verification

  • MarkdownDocumentTests: 15 passed, 0 failed on iOS Simulator
  • combined simulator pass with PR fix(swift-ios): restore thread scrolling and back swipe #6090: native selection menu appeared and full-surface back swipe returned to the thread list
  • git diff --check: passed
  • fresh direct Claude Opus 5 high review of the final diff: exit 0, no actionable findings

Checklist

  • This PR is focused on one native behavior
  • I explained what changed and why
  • Before/after screenshots are not applicable because message appearance is unchanged
  • I included a video for the interaction change

Implemented with GPT-5.6 Sol in T3 Code/Codex. Independently reviewed by Claude Opus 5 high.

Note

Enable granular text selection in markdown messages using UITextView

  • Replaces SwiftUI Text-based inline rendering with a UIViewRepresentable wrapping UITextView (MarkdownInlineText), enabling native per-word/character text selection without a toggle.
  • Adds MarkdownSelectableTextAttributes to centrally build NSAttributedString from inline markdown runs, applying dynamic type, link handling, inline code monospacing, and strikethrough.
  • Adds MarkdownSelectionRestoration to preserve the user's selection range across streaming updates when the new text safely extends the previous text.
  • Propagates selectionContext and textColor through MarkdownBlocksView, MarkdownBlockView, MarkdownTableView, MarkdownListView, and MarkdownCodeBlockView so all message content is selectable.
  • Adds a configurable copyActionTitle to MarkdownMessageView, used in both the accessibility action and the UITextView edit menu; file previews use "Copy file contents".
  • Behavioral Change: the old MarkdownTextSelectionModifier toggle is removed; text selection is always active via UIKit.

Macroscope summarized 2ae671d.


Note

Medium Risk
Large UIKit bridge in the hot-path transcript with streaming selection and layout sizing; behavior changes for table cell accessibility and keyboard dismissal on scroll.

Overview
Chat markdown now renders inline content through UITextView-backed views instead of SwiftUI Text, so users get native selection handles and the system edit menu across paragraphs, headings, lists, tables, and code bodies.

A MarkdownSelectionContext carries the full message (or file) text and a configurable copy title into each text view; the delegate adds Copy message / Copy file contents to the selection menu and keeps message-level copy via accessibility actions. MarkdownSelectionRestoration preserves the selected range while streaming only when new text extends the previous string.

Rendering moves inline styling to MarkdownSelectableTextAttributes and MarkdownInlineStyle.uiFont (Dynamic Type), adds shared UIKit theme colors for text views, and pre-renders code blocks as literal .code inline runs (reused from cache). The old context-menu “Select text” toggle is removed.

Transcript scrolling no longer calls endEditing on drag; keyboard dismissal goes through the existing onDismissKeyboard path. Tests cover formatting, Dynamic Type, code reuse, and selection restoration; home metadata tests switch to providersByEnvironment fixtures.

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

Delivery: direct
Validated against Theo commit: f98cab5
Depends on: none
Merge order: this PR only
Validation status: Mergeable; native/repository checks and current review are green. The unrelated Vercel authorization failure is a maintainer-side gate.

@coderabbitai

coderabbitaiBot commented Aug 11, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 23359412-8d23-42e6-b18b-d793f74c16fb

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

@github-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XL 500-999 changed lines (additions + deletions). labels Aug 11, 2026
@saphid
saphid marked this pull request as ready for review August 11, 2026 10:53
@saphid

Copy link
Copy Markdown
ContributorAuthor

Phone acceptance update: Alex confirmed that text selection is working on the physical phone. This PR is ready for maintainer review. The stale provider-catalog native test fixture is fixed in #6130, and this branch carries the same focused test repair; the current SwiftUI native check is green.

@cursorcursorBot left a comment

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.

Cursor Bugbot has reviewed your changes using high effort 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 2ae671d. Configure here.

block: blocks[index],
selectionContext: selectionContext,
textColor: textColor
)

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.

Dynamic Type updates get skipped

Medium Severity

MarkdownInlineText now bakes fonts from @Environment(\.dynamicTypeSize), but its parent MarkdownBlockView still uses .equatable() and only compares block, selectionContext, and textColor. When Dynamic Type changes alone, equality stays true, so body is skipped and selectable text keeps the old size. This is a regression from the previous Text + text-style Font path, which scaled without an environment read. File previews have no cell reconfigure workaround.

Additional Locations (2)
Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit 2ae671d. Configure here.

@macroscopeapp

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

This PR introduces a new granular text selection feature with significant new UIKit bridging code and selection state management (~700 lines). An unresolved review comment also identifies a potential Dynamic Type accessibility regression. Human review is recommended for this new capability.

You can customize Macroscope's approvability policy. Learn more.

@t3-codet3-codeBot left a comment

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.

one blocking Dynamic Type regression remains.

selectionContext: selectionContext,
textColor: textColor
)
.equatable()

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.

MarkdownInlineText now derives UIKit fonts from dynamicTypeSize, but this equatable boundary does not include that environment value. a Dynamic Type-only change can skip rebuilding unchanged blocks, leaving selectable text at the old size. include the size/category in the equatable inputs or remove this equality optimization for these blocks.

t3-code[bot]
t3-codeBot approved these changes Aug 12, 2026

@t3-codet3-codeBot left a comment

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.

correction after independent review: the prior Dynamic Type concern is not blocking. the UIKit representable directly reads dynamicTypeSize, includes it in its attributed-text cache key, and has focused scaling coverage. selection remains block-scoped and the many-UITextView layout cost is worth monitoring, but neither is a correctness blocker for this PR. approved.

@t3dotgg
t3dotgg merged commit 50bd5cb into pingdotgg:t3code/rebuild-mobile-app-swiftAug 15, 2026
22 of 23 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XL500-999 changed lines (additions + deletions).vouch:trustedPR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@saphid@t3dotgg
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(swift-ios): enable granular text selection - #6110

Merged
t3dotgg merged 3 commits into
pingdotgg:t3code/rebuild-mobile-app-swiftfrom
saphid:saphid/swiftui-text-selection
Aug 15, 2026
Merged

fix(swift-ios): enable granular text selection#6110
t3dotgg merged 3 commits into
pingdotgg:t3code/rebuild-mobile-app-swiftfrom
saphid:saphid/swiftui-text-selection

Conversation

@saphid

@saphidsaphid commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

What Changed

  • render Markdown paragraphs, headings, lists, tables, and code blocks with selectable native text
  • preserve bold, italic, inline-code, strike-through, link, and Dynamic Type styling
  • keep selections stable while a streaming response appends text
  • retain code-block wrapping/copy controls and route link actions through SwiftUI openURL
  • add focused coverage for formatting, Dynamic Type, code reuse, and selection restoration

Why

The native SwiftUI thread view only offered whole-message copying. People could not select a word, sentence, or code fragment with standard iOS selection handles.

This adds granular native selection while keeping message-level copy actions, streaming updates, accessibility actions, safe link routing, and the existing Markdown layout behavior.

Closes#6089.

UI Changes

Long-pressing rendered assistant prose or code now shows the native iOS selection handles and edit menu (Copy, Look Up, Translate). The appearance of messages is otherwise unchanged.

Verified in the iOS Simulator inside a live thread: native selection appeared on prose and the existing code-block controls remained available. A physical-phone acceptance pass will be attached after the approved-based combined build is installed.

Verification

  • MarkdownDocumentTests: 15 passed, 0 failed on iOS Simulator
  • combined simulator pass with PR fix(swift-ios): restore thread scrolling and back swipe #6090: native selection menu appeared and full-surface back swipe returned to the thread list
  • git diff --check: passed
  • fresh direct Claude Opus 5 high review of the final diff: exit 0, no actionable findings

Checklist

  • This PR is focused on one native behavior
  • I explained what changed and why
  • Before/after screenshots are not applicable because message appearance is unchanged
  • I included a video for the interaction change

Implemented with GPT-5.6 Sol in T3 Code/Codex. Independently reviewed by Claude Opus 5 high.

Note

Enable granular text selection in markdown messages using UITextView

  • Replaces SwiftUI Text-based inline rendering with a UIViewRepresentable wrapping UITextView (MarkdownInlineText), enabling native per-word/character text selection without a toggle.
  • Adds MarkdownSelectableTextAttributes to centrally build NSAttributedString from inline markdown runs, applying dynamic type, link handling, inline code monospacing, and strikethrough.
  • Adds MarkdownSelectionRestoration to preserve the user's selection range across streaming updates when the new text safely extends the previous text.
  • Propagates selectionContext and textColor through MarkdownBlocksView, MarkdownBlockView, MarkdownTableView, MarkdownListView, and MarkdownCodeBlockView so all message content is selectable.
  • Adds a configurable copyActionTitle to MarkdownMessageView, used in both the accessibility action and the UITextView edit menu; file previews use "Copy file contents".
  • Behavioral Change: the old MarkdownTextSelectionModifier toggle is removed; text selection is always active via UIKit.

Macroscope summarized 2ae671d.


Note

Medium Risk
Large UIKit bridge in the hot-path transcript with streaming selection and layout sizing; behavior changes for table cell accessibility and keyboard dismissal on scroll.

Overview
Chat markdown now renders inline content through UITextView-backed views instead of SwiftUI Text, so users get native selection handles and the system edit menu across paragraphs, headings, lists, tables, and code bodies.

A MarkdownSelectionContext carries the full message (or file) text and a configurable copy title into each text view; the delegate adds Copy message / Copy file contents to the selection menu and keeps message-level copy via accessibility actions. MarkdownSelectionRestoration preserves the selected range while streaming only when new text extends the previous string.

Rendering moves inline styling to MarkdownSelectableTextAttributes and MarkdownInlineStyle.uiFont (Dynamic Type), adds shared UIKit theme colors for text views, and pre-renders code blocks as literal .code inline runs (reused from cache). The old context-menu “Select text” toggle is removed.

Transcript scrolling no longer calls endEditing on drag; keyboard dismissal goes through the existing onDismissKeyboard path. Tests cover formatting, Dynamic Type, code reuse, and selection restoration; home metadata tests switch to providersByEnvironment fixtures.

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

Delivery: direct
Validated against Theo commit: f98cab5
Depends on: none
Merge order: this PR only
Validation status: Mergeable; native/repository checks and current review are green. The unrelated Vercel authorization failure is a maintainer-side gate.

@coderabbitai

coderabbitaiBot commented Aug 11, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 23359412-8d23-42e6-b18b-d793f74c16fb

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

@github-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XL 500-999 changed lines (additions + deletions). labels Aug 11, 2026
@saphid
saphid marked this pull request as ready for review August 11, 2026 10:53
@saphid

Copy link
Copy Markdown
ContributorAuthor

Phone acceptance update: Alex confirmed that text selection is working on the physical phone. This PR is ready for maintainer review. The stale provider-catalog native test fixture is fixed in #6130, and this branch carries the same focused test repair; the current SwiftUI native check is green.

@cursorcursorBot left a comment

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.

Cursor Bugbot has reviewed your changes using high effort 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 2ae671d. Configure here.

block: blocks[index],
selectionContext: selectionContext,
textColor: textColor
)

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.

Dynamic Type updates get skipped

Medium Severity

MarkdownInlineText now bakes fonts from @Environment(\.dynamicTypeSize), but its parent MarkdownBlockView still uses .equatable() and only compares block, selectionContext, and textColor. When Dynamic Type changes alone, equality stays true, so body is skipped and selectable text keeps the old size. This is a regression from the previous Text + text-style Font path, which scaled without an environment read. File previews have no cell reconfigure workaround.

Additional Locations (2)
Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit 2ae671d. Configure here.

@macroscopeapp

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

This PR introduces a new granular text selection feature with significant new UIKit bridging code and selection state management (~700 lines). An unresolved review comment also identifies a potential Dynamic Type accessibility regression. Human review is recommended for this new capability.

You can customize Macroscope's approvability policy. Learn more.

@t3-codet3-codeBot left a comment

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.

one blocking Dynamic Type regression remains.

selectionContext: selectionContext,
textColor: textColor
)
.equatable()

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.

MarkdownInlineText now derives UIKit fonts from dynamicTypeSize, but this equatable boundary does not include that environment value. a Dynamic Type-only change can skip rebuilding unchanged blocks, leaving selectable text at the old size. include the size/category in the equatable inputs or remove this equality optimization for these blocks.

t3-code[bot]
t3-codeBot approved these changes Aug 12, 2026

@t3-codet3-codeBot left a comment

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.

correction after independent review: the prior Dynamic Type concern is not blocking. the UIKit representable directly reads dynamicTypeSize, includes it in its attributed-text cache key, and has focused scaling coverage. selection remains block-scoped and the many-UITextView layout cost is worth monitoring, but neither is a correctness blocker for this PR. approved.

@t3dotgg
t3dotgg merged commit 50bd5cb into pingdotgg:t3code/rebuild-mobile-app-swiftAug 15, 2026
22 of 23 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XL500-999 changed lines (additions + deletions).vouch:trustedPR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@saphid@t3dotgg
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

fix(swift-ios): enable granular text selection - #6110

Merged
t3dotgg merged 3 commits into
pingdotgg:t3code/rebuild-mobile-app-swiftfrom
saphid:saphid/swiftui-text-selection
Aug 15, 2026
Merged

fix(swift-ios): enable granular text selection#6110
t3dotgg merged 3 commits into
pingdotgg:t3code/rebuild-mobile-app-swiftfrom
saphid:saphid/swiftui-text-selection

Conversation

@saphid

@saphidsaphid commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

What Changed

  • render Markdown paragraphs, headings, lists, tables, and code blocks with selectable native text
  • preserve bold, italic, inline-code, strike-through, link, and Dynamic Type styling
  • keep selections stable while a streaming response appends text
  • retain code-block wrapping/copy controls and route link actions through SwiftUI openURL
  • add focused coverage for formatting, Dynamic Type, code reuse, and selection restoration

Why

The native SwiftUI thread view only offered whole-message copying. People could not select a word, sentence, or code fragment with standard iOS selection handles.

This adds granular native selection while keeping message-level copy actions, streaming updates, accessibility actions, safe link routing, and the existing Markdown layout behavior.

Closes#6089.

UI Changes

Long-pressing rendered assistant prose or code now shows the native iOS selection handles and edit menu (Copy, Look Up, Translate). The appearance of messages is otherwise unchanged.

Verified in the iOS Simulator inside a live thread: native selection appeared on prose and the existing code-block controls remained available. A physical-phone acceptance pass will be attached after the approved-based combined build is installed.

Verification

  • MarkdownDocumentTests: 15 passed, 0 failed on iOS Simulator
  • combined simulator pass with PR fix(swift-ios): restore thread scrolling and back swipe #6090: native selection menu appeared and full-surface back swipe returned to the thread list
  • git diff --check: passed
  • fresh direct Claude Opus 5 high review of the final diff: exit 0, no actionable findings

Checklist

  • This PR is focused on one native behavior
  • I explained what changed and why
  • Before/after screenshots are not applicable because message appearance is unchanged
  • I included a video for the interaction change

Implemented with GPT-5.6 Sol in T3 Code/Codex. Independently reviewed by Claude Opus 5 high.

Note

Enable granular text selection in markdown messages using UITextView

  • Replaces SwiftUI Text-based inline rendering with a UIViewRepresentable wrapping UITextView (MarkdownInlineText), enabling native per-word/character text selection without a toggle.
  • Adds MarkdownSelectableTextAttributes to centrally build NSAttributedString from inline markdown runs, applying dynamic type, link handling, inline code monospacing, and strikethrough.
  • Adds MarkdownSelectionRestoration to preserve the user's selection range across streaming updates when the new text safely extends the previous text.
  • Propagates selectionContext and textColor through MarkdownBlocksView, MarkdownBlockView, MarkdownTableView, MarkdownListView, and MarkdownCodeBlockView so all message content is selectable.
  • Adds a configurable copyActionTitle to MarkdownMessageView, used in both the accessibility action and the UITextView edit menu; file previews use "Copy file contents".
  • Behavioral Change: the old MarkdownTextSelectionModifier toggle is removed; text selection is always active via UIKit.

Macroscope summarized 2ae671d.


Note

Medium Risk
Large UIKit bridge in the hot-path transcript with streaming selection and layout sizing; behavior changes for table cell accessibility and keyboard dismissal on scroll.

Overview
Chat markdown now renders inline content through UITextView-backed views instead of SwiftUI Text, so users get native selection handles and the system edit menu across paragraphs, headings, lists, tables, and code bodies.

A MarkdownSelectionContext carries the full message (or file) text and a configurable copy title into each text view; the delegate adds Copy message / Copy file contents to the selection menu and keeps message-level copy via accessibility actions. MarkdownSelectionRestoration preserves the selected range while streaming only when new text extends the previous string.

Rendering moves inline styling to MarkdownSelectableTextAttributes and MarkdownInlineStyle.uiFont (Dynamic Type), adds shared UIKit theme colors for text views, and pre-renders code blocks as literal .code inline runs (reused from cache). The old context-menu “Select text” toggle is removed.

Transcript scrolling no longer calls endEditing on drag; keyboard dismissal goes through the existing onDismissKeyboard path. Tests cover formatting, Dynamic Type, code reuse, and selection restoration; home metadata tests switch to providersByEnvironment fixtures.

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

Delivery: direct
Validated against Theo commit: f98cab5
Depends on: none
Merge order: this PR only
Validation status: Mergeable; native/repository checks and current review are green. The unrelated Vercel authorization failure is a maintainer-side gate.

@coderabbitai

coderabbitaiBot commented Aug 11, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 23359412-8d23-42e6-b18b-d793f74c16fb

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

@github-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XL 500-999 changed lines (additions + deletions). labels Aug 11, 2026
@saphid
saphid marked this pull request as ready for review August 11, 2026 10:53
@saphid

Copy link
Copy Markdown
ContributorAuthor

Phone acceptance update: Alex confirmed that text selection is working on the physical phone. This PR is ready for maintainer review. The stale provider-catalog native test fixture is fixed in #6130, and this branch carries the same focused test repair; the current SwiftUI native check is green.

@cursorcursorBot left a comment

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.

Cursor Bugbot has reviewed your changes using high effort 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 2ae671d. Configure here.

block: blocks[index],
selectionContext: selectionContext,
textColor: textColor
)

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.

Dynamic Type updates get skipped

Medium Severity

MarkdownInlineText now bakes fonts from @Environment(\.dynamicTypeSize), but its parent MarkdownBlockView still uses .equatable() and only compares block, selectionContext, and textColor. When Dynamic Type changes alone, equality stays true, so body is skipped and selectable text keeps the old size. This is a regression from the previous Text + text-style Font path, which scaled without an environment read. File previews have no cell reconfigure workaround.

Additional Locations (2)
Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit 2ae671d. Configure here.

@macroscopeapp

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

This PR introduces a new granular text selection feature with significant new UIKit bridging code and selection state management (~700 lines). An unresolved review comment also identifies a potential Dynamic Type accessibility regression. Human review is recommended for this new capability.

You can customize Macroscope's approvability policy. Learn more.

@t3-codet3-codeBot left a comment

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.

one blocking Dynamic Type regression remains.

selectionContext: selectionContext,
textColor: textColor
)
.equatable()

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.

MarkdownInlineText now derives UIKit fonts from dynamicTypeSize, but this equatable boundary does not include that environment value. a Dynamic Type-only change can skip rebuilding unchanged blocks, leaving selectable text at the old size. include the size/category in the equatable inputs or remove this equality optimization for these blocks.

t3-code[bot]
t3-codeBot approved these changes Aug 12, 2026

@t3-codet3-codeBot left a comment

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.

correction after independent review: the prior Dynamic Type concern is not blocking. the UIKit representable directly reads dynamicTypeSize, includes it in its attributed-text cache key, and has focused scaling coverage. selection remains block-scoped and the many-UITextView layout cost is worth monitoring, but neither is a correctness blocker for this PR. approved.

@t3dotgg
t3dotgg merged commit 50bd5cb into pingdotgg:t3code/rebuild-mobile-app-swiftAug 15, 2026
22 of 23 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XL500-999 changed lines (additions + deletions).vouch:trustedPR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@saphid@t3dotgg
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(swift-ios): enable granular text selection - #6110

Merged
t3dotgg merged 3 commits into
pingdotgg:t3code/rebuild-mobile-app-swiftfrom
saphid:saphid/swiftui-text-selection
Aug 15, 2026
Merged

fix(swift-ios): enable granular text selection#6110
t3dotgg merged 3 commits into
pingdotgg:t3code/rebuild-mobile-app-swiftfrom
saphid:saphid/swiftui-text-selection

Conversation

@saphid

@saphidsaphid commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

What Changed

  • render Markdown paragraphs, headings, lists, tables, and code blocks with selectable native text
  • preserve bold, italic, inline-code, strike-through, link, and Dynamic Type styling
  • keep selections stable while a streaming response appends text
  • retain code-block wrapping/copy controls and route link actions through SwiftUI openURL
  • add focused coverage for formatting, Dynamic Type, code reuse, and selection restoration

Why

The native SwiftUI thread view only offered whole-message copying. People could not select a word, sentence, or code fragment with standard iOS selection handles.

This adds granular native selection while keeping message-level copy actions, streaming updates, accessibility actions, safe link routing, and the existing Markdown layout behavior.

Closes#6089.

UI Changes

Long-pressing rendered assistant prose or code now shows the native iOS selection handles and edit menu (Copy, Look Up, Translate). The appearance of messages is otherwise unchanged.

Verified in the iOS Simulator inside a live thread: native selection appeared on prose and the existing code-block controls remained available. A physical-phone acceptance pass will be attached after the approved-based combined build is installed.

Verification

  • MarkdownDocumentTests: 15 passed, 0 failed on iOS Simulator
  • combined simulator pass with PR fix(swift-ios): restore thread scrolling and back swipe #6090: native selection menu appeared and full-surface back swipe returned to the thread list
  • git diff --check: passed
  • fresh direct Claude Opus 5 high review of the final diff: exit 0, no actionable findings

Checklist

  • This PR is focused on one native behavior
  • I explained what changed and why
  • Before/after screenshots are not applicable because message appearance is unchanged
  • I included a video for the interaction change

Implemented with GPT-5.6 Sol in T3 Code/Codex. Independently reviewed by Claude Opus 5 high.

Note

Enable granular text selection in markdown messages using UITextView

  • Replaces SwiftUI Text-based inline rendering with a UIViewRepresentable wrapping UITextView (MarkdownInlineText), enabling native per-word/character text selection without a toggle.
  • Adds MarkdownSelectableTextAttributes to centrally build NSAttributedString from inline markdown runs, applying dynamic type, link handling, inline code monospacing, and strikethrough.
  • Adds MarkdownSelectionRestoration to preserve the user's selection range across streaming updates when the new text safely extends the previous text.
  • Propagates selectionContext and textColor through MarkdownBlocksView, MarkdownBlockView, MarkdownTableView, MarkdownListView, and MarkdownCodeBlockView so all message content is selectable.
  • Adds a configurable copyActionTitle to MarkdownMessageView, used in both the accessibility action and the UITextView edit menu; file previews use "Copy file contents".
  • Behavioral Change: the old MarkdownTextSelectionModifier toggle is removed; text selection is always active via UIKit.

Macroscope summarized 2ae671d.


Note

Medium Risk
Large UIKit bridge in the hot-path transcript with streaming selection and layout sizing; behavior changes for table cell accessibility and keyboard dismissal on scroll.

Overview
Chat markdown now renders inline content through UITextView-backed views instead of SwiftUI Text, so users get native selection handles and the system edit menu across paragraphs, headings, lists, tables, and code bodies.

A MarkdownSelectionContext carries the full message (or file) text and a configurable copy title into each text view; the delegate adds Copy message / Copy file contents to the selection menu and keeps message-level copy via accessibility actions. MarkdownSelectionRestoration preserves the selected range while streaming only when new text extends the previous string.

Rendering moves inline styling to MarkdownSelectableTextAttributes and MarkdownInlineStyle.uiFont (Dynamic Type), adds shared UIKit theme colors for text views, and pre-renders code blocks as literal .code inline runs (reused from cache). The old context-menu “Select text” toggle is removed.

Transcript scrolling no longer calls endEditing on drag; keyboard dismissal goes through the existing onDismissKeyboard path. Tests cover formatting, Dynamic Type, code reuse, and selection restoration; home metadata tests switch to providersByEnvironment fixtures.

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

Delivery: direct
Validated against Theo commit: f98cab5
Depends on: none
Merge order: this PR only
Validation status: Mergeable; native/repository checks and current review are green. The unrelated Vercel authorization failure is a maintainer-side gate.

@coderabbitai

coderabbitaiBot commented Aug 11, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 23359412-8d23-42e6-b18b-d793f74c16fb

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

@github-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XL 500-999 changed lines (additions + deletions). labels Aug 11, 2026
@saphid
saphid marked this pull request as ready for review August 11, 2026 10:53
@saphid

Copy link
Copy Markdown
ContributorAuthor

Phone acceptance update: Alex confirmed that text selection is working on the physical phone. This PR is ready for maintainer review. The stale provider-catalog native test fixture is fixed in #6130, and this branch carries the same focused test repair; the current SwiftUI native check is green.

@cursorcursorBot left a comment

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.

Cursor Bugbot has reviewed your changes using high effort 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 2ae671d. Configure here.

block: blocks[index],
selectionContext: selectionContext,
textColor: textColor
)

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.

Dynamic Type updates get skipped

Medium Severity

MarkdownInlineText now bakes fonts from @Environment(\.dynamicTypeSize), but its parent MarkdownBlockView still uses .equatable() and only compares block, selectionContext, and textColor. When Dynamic Type changes alone, equality stays true, so body is skipped and selectable text keeps the old size. This is a regression from the previous Text + text-style Font path, which scaled without an environment read. File previews have no cell reconfigure workaround.

Additional Locations (2)
Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit 2ae671d. Configure here.

@macroscopeapp

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

This PR introduces a new granular text selection feature with significant new UIKit bridging code and selection state management (~700 lines). An unresolved review comment also identifies a potential Dynamic Type accessibility regression. Human review is recommended for this new capability.

You can customize Macroscope's approvability policy. Learn more.

@t3-codet3-codeBot left a comment

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.

one blocking Dynamic Type regression remains.

selectionContext: selectionContext,
textColor: textColor
)
.equatable()

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.

MarkdownInlineText now derives UIKit fonts from dynamicTypeSize, but this equatable boundary does not include that environment value. a Dynamic Type-only change can skip rebuilding unchanged blocks, leaving selectable text at the old size. include the size/category in the equatable inputs or remove this equality optimization for these blocks.

t3-code[bot]
t3-codeBot approved these changes Aug 12, 2026

@t3-codet3-codeBot left a comment

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.

correction after independent review: the prior Dynamic Type concern is not blocking. the UIKit representable directly reads dynamicTypeSize, includes it in its attributed-text cache key, and has focused scaling coverage. selection remains block-scoped and the many-UITextView layout cost is worth monitoring, but neither is a correctness blocker for this PR. approved.

@t3dotgg
t3dotgg merged commit 50bd5cb into pingdotgg:t3code/rebuild-mobile-app-swiftAug 15, 2026
22 of 23 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XL500-999 changed lines (additions + deletions).vouch:trustedPR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@saphid@t3dotgg
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

fix(swift-ios): enable granular text selection - #6110

Merged
t3dotgg merged 3 commits into
pingdotgg:t3code/rebuild-mobile-app-swiftfrom
saphid:saphid/swiftui-text-selection
Aug 15, 2026
Merged

fix(swift-ios): enable granular text selection#6110
t3dotgg merged 3 commits into
pingdotgg:t3code/rebuild-mobile-app-swiftfrom
saphid:saphid/swiftui-text-selection

Conversation

@saphid

@saphidsaphid commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

What Changed

  • render Markdown paragraphs, headings, lists, tables, and code blocks with selectable native text
  • preserve bold, italic, inline-code, strike-through, link, and Dynamic Type styling
  • keep selections stable while a streaming response appends text
  • retain code-block wrapping/copy controls and route link actions through SwiftUI openURL
  • add focused coverage for formatting, Dynamic Type, code reuse, and selection restoration

Why

The native SwiftUI thread view only offered whole-message copying. People could not select a word, sentence, or code fragment with standard iOS selection handles.

This adds granular native selection while keeping message-level copy actions, streaming updates, accessibility actions, safe link routing, and the existing Markdown layout behavior.

Closes#6089.

UI Changes

Long-pressing rendered assistant prose or code now shows the native iOS selection handles and edit menu (Copy, Look Up, Translate). The appearance of messages is otherwise unchanged.

Verified in the iOS Simulator inside a live thread: native selection appeared on prose and the existing code-block controls remained available. A physical-phone acceptance pass will be attached after the approved-based combined build is installed.

Verification

  • MarkdownDocumentTests: 15 passed, 0 failed on iOS Simulator
  • combined simulator pass with PR fix(swift-ios): restore thread scrolling and back swipe #6090: native selection menu appeared and full-surface back swipe returned to the thread list
  • git diff --check: passed
  • fresh direct Claude Opus 5 high review of the final diff: exit 0, no actionable findings

Checklist

  • This PR is focused on one native behavior
  • I explained what changed and why
  • Before/after screenshots are not applicable because message appearance is unchanged
  • I included a video for the interaction change

Implemented with GPT-5.6 Sol in T3 Code/Codex. Independently reviewed by Claude Opus 5 high.

Note

Enable granular text selection in markdown messages using UITextView

  • Replaces SwiftUI Text-based inline rendering with a UIViewRepresentable wrapping UITextView (MarkdownInlineText), enabling native per-word/character text selection without a toggle.
  • Adds MarkdownSelectableTextAttributes to centrally build NSAttributedString from inline markdown runs, applying dynamic type, link handling, inline code monospacing, and strikethrough.
  • Adds MarkdownSelectionRestoration to preserve the user's selection range across streaming updates when the new text safely extends the previous text.
  • Propagates selectionContext and textColor through MarkdownBlocksView, MarkdownBlockView, MarkdownTableView, MarkdownListView, and MarkdownCodeBlockView so all message content is selectable.
  • Adds a configurable copyActionTitle to MarkdownMessageView, used in both the accessibility action and the UITextView edit menu; file previews use "Copy file contents".
  • Behavioral Change: the old MarkdownTextSelectionModifier toggle is removed; text selection is always active via UIKit.

Macroscope summarized 2ae671d.


Note

Medium Risk
Large UIKit bridge in the hot-path transcript with streaming selection and layout sizing; behavior changes for table cell accessibility and keyboard dismissal on scroll.

Overview
Chat markdown now renders inline content through UITextView-backed views instead of SwiftUI Text, so users get native selection handles and the system edit menu across paragraphs, headings, lists, tables, and code bodies.

A MarkdownSelectionContext carries the full message (or file) text and a configurable copy title into each text view; the delegate adds Copy message / Copy file contents to the selection menu and keeps message-level copy via accessibility actions. MarkdownSelectionRestoration preserves the selected range while streaming only when new text extends the previous string.

Rendering moves inline styling to MarkdownSelectableTextAttributes and MarkdownInlineStyle.uiFont (Dynamic Type), adds shared UIKit theme colors for text views, and pre-renders code blocks as literal .code inline runs (reused from cache). The old context-menu “Select text” toggle is removed.

Transcript scrolling no longer calls endEditing on drag; keyboard dismissal goes through the existing onDismissKeyboard path. Tests cover formatting, Dynamic Type, code reuse, and selection restoration; home metadata tests switch to providersByEnvironment fixtures.

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

Delivery: direct
Validated against Theo commit: f98cab5
Depends on: none
Merge order: this PR only
Validation status: Mergeable; native/repository checks and current review are green. The unrelated Vercel authorization failure is a maintainer-side gate.

@coderabbitai

coderabbitaiBot commented Aug 11, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 23359412-8d23-42e6-b18b-d793f74c16fb

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

@github-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XL 500-999 changed lines (additions + deletions). labels Aug 11, 2026
@saphid
saphid marked this pull request as ready for review August 11, 2026 10:53
@saphid

Copy link
Copy Markdown
ContributorAuthor

Phone acceptance update: Alex confirmed that text selection is working on the physical phone. This PR is ready for maintainer review. The stale provider-catalog native test fixture is fixed in #6130, and this branch carries the same focused test repair; the current SwiftUI native check is green.

@cursorcursorBot left a comment

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.

Cursor Bugbot has reviewed your changes using high effort 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 2ae671d. Configure here.

block: blocks[index],
selectionContext: selectionContext,
textColor: textColor
)

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.

Dynamic Type updates get skipped

Medium Severity

MarkdownInlineText now bakes fonts from @Environment(\.dynamicTypeSize), but its parent MarkdownBlockView still uses .equatable() and only compares block, selectionContext, and textColor. When Dynamic Type changes alone, equality stays true, so body is skipped and selectable text keeps the old size. This is a regression from the previous Text + text-style Font path, which scaled without an environment read. File previews have no cell reconfigure workaround.

Additional Locations (2)
Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit 2ae671d. Configure here.

@macroscopeapp

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

This PR introduces a new granular text selection feature with significant new UIKit bridging code and selection state management (~700 lines). An unresolved review comment also identifies a potential Dynamic Type accessibility regression. Human review is recommended for this new capability.

You can customize Macroscope's approvability policy. Learn more.

@t3-codet3-codeBot left a comment

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.

one blocking Dynamic Type regression remains.

selectionContext: selectionContext,
textColor: textColor
)
.equatable()

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.

MarkdownInlineText now derives UIKit fonts from dynamicTypeSize, but this equatable boundary does not include that environment value. a Dynamic Type-only change can skip rebuilding unchanged blocks, leaving selectable text at the old size. include the size/category in the equatable inputs or remove this equality optimization for these blocks.

t3-code[bot]
t3-codeBot approved these changes Aug 12, 2026

@t3-codet3-codeBot left a comment

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.

correction after independent review: the prior Dynamic Type concern is not blocking. the UIKit representable directly reads dynamicTypeSize, includes it in its attributed-text cache key, and has focused scaling coverage. selection remains block-scoped and the many-UITextView layout cost is worth monitoring, but neither is a correctness blocker for this PR. approved.

@t3dotgg
t3dotgg merged commit 50bd5cb into pingdotgg:t3code/rebuild-mobile-app-swiftAug 15, 2026
22 of 23 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XL500-999 changed lines (additions + deletions).vouch:trustedPR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@saphid@t3dotgg
, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

fix(swift-ios): enable granular text selection - #6110

Merged
t3dotgg merged 3 commits into
pingdotgg:t3code/rebuild-mobile-app-swiftfrom
saphid:saphid/swiftui-text-selection
Aug 15, 2026
Merged

fix(swift-ios): enable granular text selection#6110
t3dotgg merged 3 commits into
pingdotgg:t3code/rebuild-mobile-app-swiftfrom
saphid:saphid/swiftui-text-selection

Conversation

@saphid

@saphidsaphid commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

What Changed

  • render Markdown paragraphs, headings, lists, tables, and code blocks with selectable native text
  • preserve bold, italic, inline-code, strike-through, link, and Dynamic Type styling
  • keep selections stable while a streaming response appends text
  • retain code-block wrapping/copy controls and route link actions through SwiftUI openURL
  • add focused coverage for formatting, Dynamic Type, code reuse, and selection restoration

Why

The native SwiftUI thread view only offered whole-message copying. People could not select a word, sentence, or code fragment with standard iOS selection handles.

This adds granular native selection while keeping message-level copy actions, streaming updates, accessibility actions, safe link routing, and the existing Markdown layout behavior.

Closes#6089.

UI Changes

Long-pressing rendered assistant prose or code now shows the native iOS selection handles and edit menu (Copy, Look Up, Translate). The appearance of messages is otherwise unchanged.

Verified in the iOS Simulator inside a live thread: native selection appeared on prose and the existing code-block controls remained available. A physical-phone acceptance pass will be attached after the approved-based combined build is installed.

Verification

  • MarkdownDocumentTests: 15 passed, 0 failed on iOS Simulator
  • combined simulator pass with PR fix(swift-ios): restore thread scrolling and back swipe #6090: native selection menu appeared and full-surface back swipe returned to the thread list
  • git diff --check: passed
  • fresh direct Claude Opus 5 high review of the final diff: exit 0, no actionable findings

Checklist

  • This PR is focused on one native behavior
  • I explained what changed and why
  • Before/after screenshots are not applicable because message appearance is unchanged
  • I included a video for the interaction change

Implemented with GPT-5.6 Sol in T3 Code/Codex. Independently reviewed by Claude Opus 5 high.

Note

Enable granular text selection in markdown messages using UITextView

  • Replaces SwiftUI Text-based inline rendering with a UIViewRepresentable wrapping UITextView (MarkdownInlineText), enabling native per-word/character text selection without a toggle.
  • Adds MarkdownSelectableTextAttributes to centrally build NSAttributedString from inline markdown runs, applying dynamic type, link handling, inline code monospacing, and strikethrough.
  • Adds MarkdownSelectionRestoration to preserve the user's selection range across streaming updates when the new text safely extends the previous text.
  • Propagates selectionContext and textColor through MarkdownBlocksView, MarkdownBlockView, MarkdownTableView, MarkdownListView, and MarkdownCodeBlockView so all message content is selectable.
  • Adds a configurable copyActionTitle to MarkdownMessageView, used in both the accessibility action and the UITextView edit menu; file previews use "Copy file contents".
  • Behavioral Change: the old MarkdownTextSelectionModifier toggle is removed; text selection is always active via UIKit.

Macroscope summarized 2ae671d.


Note

Medium Risk
Large UIKit bridge in the hot-path transcript with streaming selection and layout sizing; behavior changes for table cell accessibility and keyboard dismissal on scroll.

Overview
Chat markdown now renders inline content through UITextView-backed views instead of SwiftUI Text, so users get native selection handles and the system edit menu across paragraphs, headings, lists, tables, and code bodies.

A MarkdownSelectionContext carries the full message (or file) text and a configurable copy title into each text view; the delegate adds Copy message / Copy file contents to the selection menu and keeps message-level copy via accessibility actions. MarkdownSelectionRestoration preserves the selected range while streaming only when new text extends the previous string.

Rendering moves inline styling to MarkdownSelectableTextAttributes and MarkdownInlineStyle.uiFont (Dynamic Type), adds shared UIKit theme colors for text views, and pre-renders code blocks as literal .code inline runs (reused from cache). The old context-menu “Select text” toggle is removed.

Transcript scrolling no longer calls endEditing on drag; keyboard dismissal goes through the existing onDismissKeyboard path. Tests cover formatting, Dynamic Type, code reuse, and selection restoration; home metadata tests switch to providersByEnvironment fixtures.

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

Delivery: direct
Validated against Theo commit: f98cab5
Depends on: none
Merge order: this PR only
Validation status: Mergeable; native/repository checks and current review are green. The unrelated Vercel authorization failure is a maintainer-side gate.

@coderabbitai

coderabbitaiBot commented Aug 11, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 23359412-8d23-42e6-b18b-d793f74c16fb

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

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.

@github-actionsgithub-actionsBot added vouch:trusted PR author is trusted by repo permissions or the VOUCHED list. size:XL 500-999 changed lines (additions + deletions). labels Aug 11, 2026
@saphid
saphid marked this pull request as ready for review August 11, 2026 10:53
@saphid

Copy link
Copy Markdown
ContributorAuthor

Phone acceptance update: Alex confirmed that text selection is working on the physical phone. This PR is ready for maintainer review. The stale provider-catalog native test fixture is fixed in #6130, and this branch carries the same focused test repair; the current SwiftUI native check is green.

@cursorcursorBot left a comment

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.

Cursor Bugbot has reviewed your changes using high effort 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 2ae671d. Configure here.

block: blocks[index],
selectionContext: selectionContext,
textColor: textColor
)

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.

Dynamic Type updates get skipped

Medium Severity

MarkdownInlineText now bakes fonts from @Environment(\.dynamicTypeSize), but its parent MarkdownBlockView still uses .equatable() and only compares block, selectionContext, and textColor. When Dynamic Type changes alone, equality stays true, so body is skipped and selectable text keeps the old size. This is a regression from the previous Text + text-style Font path, which scaled without an environment read. File previews have no cell reconfigure workaround.

Additional Locations (2)
Fix in CursorFix in Web

Reviewed by Cursor Bugbot for commit 2ae671d. Configure here.

@macroscopeapp

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Needs human review

This PR introduces a new granular text selection feature with significant new UIKit bridging code and selection state management (~700 lines). An unresolved review comment also identifies a potential Dynamic Type accessibility regression. Human review is recommended for this new capability.

You can customize Macroscope's approvability policy. Learn more.

@t3-codet3-codeBot left a comment

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.

one blocking Dynamic Type regression remains.

selectionContext: selectionContext,
textColor: textColor
)
.equatable()

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.

MarkdownInlineText now derives UIKit fonts from dynamicTypeSize, but this equatable boundary does not include that environment value. a Dynamic Type-only change can skip rebuilding unchanged blocks, leaving selectable text at the old size. include the size/category in the equatable inputs or remove this equality optimization for these blocks.

t3-code[bot]
t3-codeBot approved these changes Aug 12, 2026

@t3-codet3-codeBot left a comment

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.

correction after independent review: the prior Dynamic Type concern is not blocking. the UIKit representable directly reads dynamicTypeSize, includes it in its attributed-text cache key, and has focused scaling coverage. selection remains block-scoped and the many-UITextView layout cost is worth monitoring, but neither is a correctness blocker for this PR. approved.

@t3dotgg
t3dotgg merged commit 50bd5cb into pingdotgg:t3code/rebuild-mobile-app-swiftAug 15, 2026
22 of 23 checks passed
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:XL500-999 changed lines (additions + deletions).vouch:trustedPR author is trusted by repo permissions or the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants

@saphid@t3dotgg