Skip to content

Replace markdown pane with STTextView - #2973

Closed
lawrencecchen wants to merge 1 commit into
mainfrom
task-sttextview-markdown-pane
Closed

lawrencecchen wants to merge 1 commit into
mainfrom
task-sttextview-markdown-pane

Conversation

@lawrencecchen

@lawrencecchen lawrencecchen commented Apr 18, 2026 •

Copy link
Copy Markdown
Contributor

Replaces the markdown pane implementation with STTextView.

What changed:

  • swaps the package dependency from MarkdownUI to STTextView
  • renders markdown into a read-only STTextView surface
  • keeps Ghostty font family, font size, and background/foreground colors
  • enables line numbers in the markdown pane
  • keeps link attributes and opens links on cmd-click
  • adds focused unit coverage for renderer styling, theme application, and cmd-click link policy

Verification:

  • xcodebuild -project GhosttyTabs.xcodeproj -scheme cmux-unit -destination 'platform=macOS' -derivedDataPath /tmp/cmux-sttext-unit test -only-testing:cmuxTests/MarkdownPanelTextViewTests
  • ./scripts/reload.sh --tag sttext

Summary by cubic

Replaces the markdown pane with STTextView for native text rendering while keeping the Ghostty look and feel. Adds line numbers and cmd-click to open links, and removes MarkdownUI.

  • New Features

    • Read-only STTextView markdown surface with Ghostty font, size, and colors.
    • Line numbers enabled; links keep attributes and open on cmd-click.
    • Preserves scroll position on content/theme changes; adds focused tests for styling, theming, and link policy.
  • Dependencies

    • Swap MarkdownUI → STTextView (adds CoreTextSwift and STTextKitPlus).
    • Update Xcode project and Package.resolved.

Written for commit 48cc411. Summary will update on new commits.

Summary by CodeRabbit

  • Refactor

    • Updated markdown panel rendering with improved text display component.
    • Links in markdown panels now require Command modifier to open.
  • Tests

    • Added comprehensive test coverage for markdown panel functionality.

@vercel

vercel Bot commented Apr 18, 2026 •

Copy link
Copy Markdown

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

Project Deployment Actions Updated (UTC)
cmux Ready Ready Preview, Comment Apr 18, 2026 3:31am

@coderabbitai

coderabbitai Bot commented Apr 18, 2026 •

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This PR replaces the MarkdownUI package dependency with STTextView for markdown rendering and refactors the MarkdownPanelView to use a new attributed-string-based rendering pipeline with custom theme and editor configuration classes, while adding corresponding test coverage.

Changes

Cohort / File(s) Summary
Build & Package Dependencies
GhosttyTabs.xcodeproj/project.pbxproj, GhosttyTabs.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
Updated Swift package dependency from MarkdownUI to STTextView; replaced transitive dependencies (NetworkImage, swift-cmark) with CoreTextSwift and STTextKitPlus; added new unit test target build entries.
Markdown Rendering Refactor
Sources/Panels/MarkdownPanelView.swift
Replaced MarkdownUI-based rendering with STTextView-backed pipeline; introduced MarkdownPanelAttributedRenderer, MarkdownPanelTheme, MarkdownPanelEditorConfiguration, MarkdownPanelTextSurface, and MarkdownPanelSTTextContainerView types; implemented custom link activation handling, update diffing, and scroll position preservation; removed file path rendering in unavailable state; simplified onChange observers.
Test Suite
cmuxTests/MarkdownPanelTextViewTests.swift
Added MarkdownPanelTextViewTests suite with four test cases validating attributed string rendering, editor configuration, link activation policy, and theme application for the new markdown rendering system.

Sequence Diagram

sequenceDiagram
    participant MarkdownPanelView
    participant MarkdownPanelTextSurface
    participant MarkdownPanelAttributedRenderer
    participant MarkdownPanelTheme
    participant MarkdownPanelSTTextContainerView as Container View
    participant STTextView

    MarkdownPanelView->>MarkdownPanelTextSurface: markdown, ghosttyConfig, isFocused
    MarkdownPanelTextSurface->>MarkdownPanelAttributedRenderer: render markdown content
    MarkdownPanelAttributedRenderer-->>MarkdownPanelTextSurface: attributed string
    MarkdownPanelTextSurface->>MarkdownPanelTheme: create theme from ghosttyConfig
    MarkdownPanelTheme-->>MarkdownPanelTextSurface: theme object
    MarkdownPanelTextSurface->>MarkdownPanelSTTextContainerView: configure with string & theme
    MarkdownPanelSTTextContainerView->>STTextView: set attributed text
    MarkdownPanelSTTextContainerView->>STTextView: apply theme styles
    MarkdownPanelSTTextContainerView->>STTextView: set focus state
    STTextView-->>Container View: display rendered markdown
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

Poem

🐰 A markdown shift with nibbling grace,
STTextView takes the rendering place.
With styled strings and themes so fine,
The rabbit hops through each design—
New tests ensure all links align! ✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Replace markdown pane with STTextView' accurately summarizes the main change—switching the markdown rendering from MarkdownUI to STTextView.
Description check ✅ Passed The description covers the required sections with clear information about changes, reasoning, and includes testing verification steps.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch task-sttextview-markdown-pane

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

❤️ Share

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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 48cc411913

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

}
guard level > 0,
source.length > level,
CharacterSet.whitespaces.contains(UnicodeScalar(source.character(at: level))!) else {

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Guard surrogate scalars when checking heading whitespace

Avoid force-unwrapping UnicodeScalar(...) here: a line like #😀 (hash followed by an emoji, i.e. a surrogate pair) makes UnicodeScalar(source.character(at: level)) return nil, which crashes the renderer while processing otherwise valid markdown text. This path runs for every line in attributedString(markdown:theme:), so a single such line can take down the markdown panel.

Useful? React with 👍 / 👎.

}

enum MarkdownPanelAttributedRenderer {
private static let markdownLinkRegex = makeRegex(#"\[([^\]]+)\]\(([^)\s]+)\)"#)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Support link destinations containing parentheses

The markdown link regex stops the destination at the first ) and rejects many legal link forms, so links such as [wiki](https://en.wikipedia.org/wiki/Function_(mathematics)) are parsed as truncated URLs and cmd-click opens the wrong target (or no target). Since link activation now depends on this parser, this introduces a functional regression for common markdown links.

Useful? React with 👍 / 👎.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
Sources/Panels/MarkdownPanelView.swift (1)

636-659: Avoid force-unwrapping a UnicodeScalar built from a UTF‑16 code unit.

UnicodeScalar(source.character(at: level))! will crash if the character at that index is a surrogate half. While unlikely for a character following # in a heading, the cheaper and safer form is CharacterSet.whitespaces.contains(Unicode.Scalar(source.character(at: level)) ?? Unicode.Scalar(0)), or simply test against 0x20/0x09 directly. Low severity — included for defense-in-depth.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/Panels/MarkdownPanelView.swift` around lines 636 - 659, In
headingMetadata(in:) avoid force-unwrapping UnicodeScalar built from UTF-16 by
replacing CharacterSet.whitespaces.contains(UnicodeScalar(source.character(at:
level))!) with a safe check; e.g. create a safe scalar using let scalar =
Unicode.Scalar(source.character(at: level)) ?? Unicode.Scalar(0) and call
CharacterSet.whitespaces.contains(scalar), or alternatively compare the code
unit directly (e.g. let ch = source.character(at: level); guard ch == 0x20 || ch
== 0x09 else { return nil }) to eliminate the force unwrap in headingMetadata.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@Sources/Panels/MarkdownPanelView.swift`:
- Around line 523-531: The current pipe heuristic uses lineText.contains("|")
(in the block that calls attributed.addAttributes with theme.codeFont and
theme.textColor over lineContentRange) and thus misclassifies prose with single
pipes or operators as tables; update the check to require a more reliable table
pattern (e.g., at least two '|' characters and a following separator row like
/^\s*\|.*\|\s*$/ plus a subsequent line matching a separator such as /^\s*\|?[-:
]+\|[-: |]*$/) before applying table styling, or disable this heuristic entirely
and only apply theme.codeFont when a full table parse is confirmed. Ensure the
change touches the same conditional that currently uses lineText.contains("|")
and still uses attributed.addAttributes with lineContentRange when the stricter
condition is met.
- Around line 216-225: The current link-activation flow calls
NSWorkspace.shared.open(url) without validating the URL scheme; restrict which
schemes are allowed before opening by extracting url.scheme (lowercased) and
allowlisting only "http", "https", "mailto" (and "file" only if you intend to
support local files), and return true without calling
NSWorkspace.shared.open(url) for any other schemes; update the block around
MarkdownPanelLinkActivationPolicy.url(for:) / shouldOpenLink (referencing
MarkdownPanelSTTextView and NSWorkspace.shared.open(url)) to perform this scheme
check and early-return when the scheme is not allowed.
- Around line 179-200: The restoreScrollPosition(previousRatio:) call happens
immediately after setting textView.attributedText while STTextView lays out
asynchronously, so use a deferred restore once layout is finished: after setting
textView.attributedText in the contentChanged branch, request/ensure layout with
textView.textLayoutManager?.ensureLayout(for:) or observe
NSTextLayoutManagerDelegate callbacks (or as a simpler fallback dispatch to the
next run loop with DispatchQueue.main.async) and then call
restoreScrollPosition(previousRatio:); keep currentMarkdown assignment after
successful restore and leave the existing theme-only branch unchanged.

---

Nitpick comments:
In `@Sources/Panels/MarkdownPanelView.swift`:
- Around line 636-659: In headingMetadata(in:) avoid force-unwrapping
UnicodeScalar built from UTF-16 by replacing
CharacterSet.whitespaces.contains(UnicodeScalar(source.character(at: level))!)
with a safe check; e.g. create a safe scalar using let scalar =
Unicode.Scalar(source.character(at: level)) ?? Unicode.Scalar(0) and call
CharacterSet.whitespaces.contains(scalar), or alternatively compare the code
unit directly (e.g. let ch = source.character(at: level); guard ch == 0x20 || ch
== 0x09 else { return nil }) to eliminate the force unwrap in headingMetadata.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 87775ecb-8fb3-4e9c-a227-80b645ae6f5b

📥 Commits

Reviewing files that changed from the base of the PR and between ea438e1 and 48cc411.

📒 Files selected for processing (4)
  • GhosttyTabs.xcodeproj/project.pbxproj
  • GhosttyTabs.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved
  • Sources/Panels/MarkdownPanelView.swift
  • cmuxTests/MarkdownPanelTextViewTests.swift

Comment on lines +179 to +200
let previousOrigin = scrollView.contentView.bounds.origin
let previousViewportHeight = scrollView.contentView.bounds.height
let previousDocumentHeight = textView.frame.height
let previousScrollRatio = markdownScrollRatio(
originY: previousOrigin.y,
documentHeight: previousDocumentHeight,
viewportHeight: previousViewportHeight
)

MarkdownPanelEditorConfiguration.apply(theme: theme, to: textView, scrollView: scrollView)

if themeChanged || contentChanged {
textView.attributedText = MarkdownPanelAttributedRenderer.attributedString(markdown: markdown, theme: theme)
}

if contentChanged {
restoreScrollPosition(previousRatio: previousScrollRatio)
currentMarkdown = markdown
} else if themeChanged {
scrollView.contentView.scroll(to: previousOrigin)
scrollView.reflectScrolledClipView(scrollView.contentView)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Scroll-position restore may jump after content updates.

restoreScrollPosition(previousRatio:) reads textView.frame.height immediately after textView.attributedText = .... STTextView lays out text asynchronously via NSTextLayoutManager, so frame.height often still reflects the previous document. The computed scrollableHeight and restored y will be based on stale geometry, causing a visible jump (typically to the top) on content updates.

Consider deferring the restore until after layout completes (e.g., using textView.textLayoutManager?.ensureLayout(for:) plus NSTextLayoutManagerDelegate, or dispatching to the next run loop and reapplying), or anchoring to a text location rather than a ratio.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/Panels/MarkdownPanelView.swift` around lines 179 - 200, The
restoreScrollPosition(previousRatio:) call happens immediately after setting
textView.attributedText while STTextView lays out asynchronously, so use a
deferred restore once layout is finished: after setting textView.attributedText
in the contentChanged branch, request/ensure layout with
textView.textLayoutManager?.ensureLayout(for:) or observe
NSTextLayoutManagerDelegate callbacks (or as a simpler fallback dispatch to the
next run loop with DispatchQueue.main.async) and then call
restoreScrollPosition(previousRatio:); keep currentMarkdown assignment after
successful restore and leave the existing theme-only branch unchanged.

Comment on lines +216 to +225
guard let url = MarkdownPanelLinkActivationPolicy.url(for: link) else {
return true
}
guard MarkdownPanelLinkActivationPolicy.shouldOpenLink(
modifierFlags: (textView as? MarkdownPanelSTTextView)?.lastMouseDownModifierFlags ?? []
) else {
return true
}
NSWorkspace.shared.open(url)
return true

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Restrict URL schemes before opening.

Markdown content is read from arbitrary files on disk and may contain javascript:, file://, or other unexpected schemes. NSWorkspace.shared.open(url) will happily dispatch those to registered handlers. Consider allowlisting to http, https, mailto (and perhaps file only for intentional cases) before calling open.

Proposed guard
-        NSWorkspace.shared.open(url)
-        return true
+        let allowedSchemes: Set<String> = ["http", "https", "mailto"]
+        if let scheme = url.scheme?.lowercased(), allowedSchemes.contains(scheme) {
+            NSWorkspace.shared.open(url)
+        }
+        return true
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/Panels/MarkdownPanelView.swift` around lines 216 - 225, The current
link-activation flow calls NSWorkspace.shared.open(url) without validating the
URL scheme; restrict which schemes are allowed before opening by extracting
url.scheme (lowercased) and allowlisting only "http", "https", "mailto" (and
"file" only if you intend to support local files), and return true without
calling NSWorkspace.shared.open(url) for any other schemes; update the block
around MarkdownPanelLinkActivationPolicy.url(for:) / shouldOpenLink (referencing
MarkdownPanelSTTextView and NSWorkspace.shared.open(url)) to perform this scheme
check and early-return when the scheme is not allowed.

Comment on lines +523 to +531
} else if lineText.contains("|") {
attributed.addAttributes(
[
.font: theme.codeFont,
.foregroundColor: theme.textColor
],
range: lineContentRange
)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Pipe-heuristic for tables over-matches prose.

lineText.contains("|") styles any prose line containing a pipe (e.g., use \| to escape, shell examples, if a || b) as a table, applying codeFont and removing paragraph styling. Consider requiring at least two pipes and a separator row (e.g., |---|---|) before treating a line as a table, or skip table styling entirely until a proper parser is used.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Sources/Panels/MarkdownPanelView.swift` around lines 523 - 531, The current
pipe heuristic uses lineText.contains("|") (in the block that calls
attributed.addAttributes with theme.codeFont and theme.textColor over
lineContentRange) and thus misclassifies prose with single pipes or operators as
tables; update the check to require a more reliable table pattern (e.g., at
least two '|' characters and a following separator row like /^\s*\|.*\|\s*$/
plus a subsequent line matching a separator such as /^\s*\|?[-: ]+\|[-: |]*$/)
before applying table styling, or disable this heuristic entirely and only apply
theme.codeFont when a full table parse is confirmed. Ensure the change touches
the same conditional that currently uses lineText.contains("|") and still uses
attributed.addAttributes with lineContentRange when the stricter condition is
met.

@cubic-dev-ai cubic-dev-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

1 issue found across 4 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="Sources/Panels/MarkdownPanelView.swift">

<violation number="1" location="Sources/Panels/MarkdownPanelView.swift:644">
P2: Force-unwrapping `UnicodeScalar(source.character(at: level))` can crash on surrogate code units. Use a safe unwrap instead.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

}
guard level > 0,
source.length > level,
CharacterSet.whitespaces.contains(UnicodeScalar(source.character(at: level))!) else {

@cubic-dev-ai cubic-dev-ai Bot Apr 18, 2026 •

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2: Force-unwrapping UnicodeScalar(source.character(at: level)) can crash on surrogate code units. Use a safe unwrap instead.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Sources/Panels/MarkdownPanelView.swift, line 644:

<comment>Force-unwrapping `UnicodeScalar(source.character(at: level))` can crash on surrogate code units. Use a safe unwrap instead.</comment>

<file context>
@@ -290,6 +108,631 @@ struct MarkdownPanelView: View {
+        }
+        guard level > 0,
+              source.length > level,
+              CharacterSet.whitespaces.contains(UnicodeScalar(source.character(at: level))!) else {
+            return nil
+        }
</file context>
Suggested change
CharacterSet.whitespaces.contains(UnicodeScalar(source.character(at: level))!) else {
let scalar = UnicodeScalar(source.character(at: level)),
CharacterSet.whitespaces.contains(scalar) else {
Fix with Cubic

@greptile-apps

greptile-apps Bot commented Apr 18, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

Replaces the MarkdownUI dependency with a custom NSAttributedString-based renderer over STTextView, displaying markdown as a syntax-highlighted source view (raw markers visible) rather than a fully-rendered document. The implementation handles Ghostty theme integration, scroll-position preservation across content/theme updates, and cmd-click link activation, and ships focused unit coverage.

  • P1 – crash on emoji headings: UnicodeScalar(source.character(at: level))! in headingMetadata force-unwraps without checking for UTF-16 surrogate values; a heading line where the character right after ## is a non-BMP scalar (leading surrogate) will crash the process. Fix: use UnicodeScalar(UInt32(...)) with optional binding before the whitespace check.
  • P2 – table heuristic too broad: lineText.contains(\"|\") classifies any prose line with a pipe as a table row and renders it in code font; tighten to require ≥ 2 pipe characters or a leading |.
  • P2 – cursor color mismatch: insertionPointColor is set to linkColor; GhosttyConfig.cursorColor is already tracked in the cache key but never exposed as a theme property or applied.

Confidence Score: 4/5

Safe to merge after fixing the force-unwrap crash in headingMetadata; all other findings are P2 style/quality improvements.

One P1 crash exists in headingMetadata where a non-BMP character immediately after heading markers will hit a nil force-unwrap. The remaining issues (cursor color mismatch, overly-broad table heuristic) are P2 and do not block correctness on typical markdown inputs. The overall architecture is sound, scroll preservation and cmd-click link handling are well-structured, and the unit tests cover the key behavioral contracts.

Sources/Panels/MarkdownPanelView.swift — headingMetadata force-unwrap (line ~672) and table heuristic (line ~523)

Important Files Changed

Filename Overview
Sources/Panels/MarkdownPanelView.swift Major rewrite: replaces MarkdownUI with a custom NSAttributedString renderer over STTextView; adds scroll-position preservation, cmd-click link policy, and Ghostty-theme integration; a few minor issues with the heading parser and heuristic table detection.
cmuxTests/MarkdownPanelTextViewTests.swift New unit tests covering renderer styling, editor configuration, and cmd-click link policy; tests exercise real runtime behavior through executable paths, consistent with the project's test quality policy.
GhosttyTabs.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved Swaps MarkdownUI/swift-cmark/NetworkImage pins for STTextView/STTextKitPlus/CoreTextSwift; version pinning looks correct.
GhosttyTabs.xcodeproj/project.pbxproj Package dependency references updated to match the resolved pin swap; no structural project file concerns.

Sequence Diagram

sequenceDiagram
    participant SwiftUI as SwiftUI (MarkdownPanelView)
    participant Surface as MarkdownPanelTextSurface
    participant Container as MarkdownPanelSTTextContainerView
    participant Renderer as MarkdownPanelAttributedRenderer
    participant Theme as MarkdownPanelTheme
    participant TextView as MarkdownPanelSTTextView

    SwiftUI->>Surface: updateNSView(markdown, config, isFocused)
    Surface->>Container: update(markdown, ghosttyConfig, isFocused)
    Container->>Theme: init(config) → cacheKey
    Container->>Container: detect themeChanged / contentChanged / focusChanged
    alt themeChanged or contentChanged
        Container->>Renderer: attributedString(markdown, theme)
        Renderer-->>Container: NSAttributedString
        Container->>TextView: attributedText = …
    end
    alt themeChanged
        Container->>TextView: apply(theme, textView, scrollView)
    end
    alt contentChanged
        Container->>Container: restoreScrollPosition(previousRatio)
    end
    alt isFocused && focusChanged
        Container->>TextView: makeFirstResponder
    end
    TextView-->>SwiftUI: rendered view

    Note over TextView: On mouseDown
    TextView->>TextView: capture lastMouseDownModifierFlags
    TextView->>Container: clickedOnLink(link, location)
    Container->>Container: check .command modifier
    alt cmd held
        Container->>Container: NSWorkspace.open(url)
    end
    TextView->>TextView: clear lastMouseDownModifierFlags
Loading

Reviews (1): Last reviewed commit: "Replace markdown pane with STTextView" | Re-trigger Greptile

if let match = orderedListRegex.firstMatch(in: lineText, range: fullRange) {
return match.range
}
return nil

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.

P1 Force-unwrap crash on emoji headings

source.character(at: level) returns a unichar (UInt16). For any character outside the Basic Multilingual Plane (emoji, certain CJK characters), NSString represents it as a UTF-16 surrogate pair. If the character immediately after the # markers happens to be a leading surrogate (0xD800–0xDBFF) — for example ##😀 No space — UnicodeScalar(surrogate) returns nil and the ! crashes the process.

The guard already correctly rejects ##emoji (no space) by the whitespace check failing, but the crash fires before the check can return false. Replace with an optional-safe path:

Suggested change
return nil
let scalar = UnicodeScalar(UInt32(source.character(at: level)))
guard level > 0,
source.length > level,
let scalar,
CharacterSet.whitespaces.contains(scalar) else {

textView.font = theme.font
textView.textColor = theme.textColor
textView.backgroundColor = theme.editorBackgroundColor
textView.insertionPointColor = theme.linkColor

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 insertionPointColor uses link color instead of cursor color

GhosttyConfig.cursorColor is captured in the theme's cacheKey (and thus tracked for invalidation), but it's never surfaced as a theme field. Instead the insertion-point color is set to linkColor, which is a semantic mismatch. For a read-only view the cursor is rarely visible, but if the view ever receives focus the user will see the link color as the caret.

Suggested change
textView.insertionPointColor = theme.linkColor
textView.insertionPointColor = theme.cursorColor

(Requires adding let cursorColor: NSColor to MarkdownPanelTheme and setting it to config.cursorColor in init.)

Comment on lines +523 to +530
} else if lineText.contains("|") {
attributed.addAttributes(
[
.font: theme.codeFont,
.foregroundColor: theme.textColor
],
range: lineContentRange
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

P2 Overly broad table-row heuristic misclassifies prose

Any line that contains a literal | is rendered in codeFont. This catches genuine GFM table rows but also sentences like "use A | B to toggle…" or shell commands in prose. CommonMark/GFM table rows must have at least two |-delimited cells or start/end with |, so a tighter check would reduce false positives:

Suggested change
} else if lineText.contains("|") {
attributed.addAttributes(
[
.font: theme.codeFont,
.foregroundColor: theme.textColor
],
range: lineContentRange
)
} else if lineText.hasPrefix("|") || (lineText.contains("|") && lineText.filter({ $0 == "|" }).count >= 2) {

@lawrencecchen lawrencecchen added the stale-revisit Closed after 30+ days without activity; preserved for possible revisit or reopening. label Sep 23, 2026
@github-project-automation github-project-automation Bot moved this from Todo to Done in cmux backlog Sep 23, 2026

This branch was successfully deployed

1 active deployment
Preview — 48cc4119 Deployed Apr 18, 2026 by vercel[bot]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

stale-revisit Closed after 30+ days without activity; preserved for possible revisit or reopening.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants