Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,30 @@ jobs:
p12-file-base64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }}
p12-password: ${{ secrets.P12_PASSWORD }}

- name: Install provisioning profile
env:
MACOS_PROVISIONING_PROFILE_BASE64: ${{ secrets.MACOS_PROVISIONING_PROFILE_BASE64 }}
run: |
if [ -z "$MACOS_PROVISIONING_PROFILE_BASE64" ]; then
echo "::error::MACOS_PROVISIONING_PROFILE_BASE64 is required."
exit 1
fi

PROFILE_PATH="$RUNNER_TEMP/RxCode.provisionprofile"
PROFILE_PLIST="$RUNNER_TEMP/RxCode-profile.plist"
PROFILE_DIR="$HOME/Library/MobileDevice/Provisioning Profiles"

mkdir -p "$PROFILE_DIR"
printf '%s' "$MACOS_PROVISIONING_PROFILE_BASE64" | base64 -D > "$PROFILE_PATH"
openssl cms -inform DER -verify -noverify -in "$PROFILE_PATH" -out "$PROFILE_PLIST"

PROFILE_UUID=$(/usr/libexec/PlistBuddy -c "Print UUID" "$PROFILE_PLIST")
PROFILE_NAME=$(/usr/libexec/PlistBuddy -c "Print Name" "$PROFILE_PLIST")

cp "$PROFILE_PATH" "$PROFILE_DIR/$PROFILE_UUID.provisionprofile"
echo "MACOS_PROFILE_UUID=$PROFILE_UUID" >> "$GITHUB_ENV"
echo "MACOS_PROFILE_SPECIFIER=$PROFILE_NAME" >> "$GITHUB_ENV"

- name: Write Firebase config
env:
FIREBASE_MACOS_B64: ${{ secrets.FIREBASE_MACOS_B64 }}
Expand Down Expand Up @@ -74,6 +98,8 @@ jobs:
-allowProvisioningUpdates \
CODE_SIGN_IDENTITY="${{ secrets.SIGNING_CERTIFICATE_NAME }}" \
CODE_SIGN_STYLE=Manual \
PROVISIONING_PROFILE="$MACOS_PROFILE_UUID" \
PROVISIONING_PROFILE_SPECIFIER="$MACOS_PROFILE_SPECIFIER" \
OTHER_CODE_SIGN_FLAGS="--options=runtime --timestamp" \
CURRENT_PROJECT_VERSION=${{ github.run_number }} \
archive | xcpretty
Expand Down
110 changes: 105 additions & 5 deletions Packages/Sources/RxCodeChatKit/MarkdownView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@ import Foundation
import MarkdownUI
import RxCodeCore
import Textual
#if canImport(AppKit)
import AppKit
#elseif canImport(UIKit)
import UIKit
#endif

// MARK: - Markdown Content View

Expand All @@ -11,11 +16,18 @@ public struct MarkdownContentView: View {
let text: String
let showsTrailingCursor: Bool
let isCursorVisible: Bool

public init(text: String, showsTrailingCursor: Bool = false, isCursorVisible: Bool = true) {
let baseURL: URL?

public init(
text: String,
showsTrailingCursor: Bool = false,
isCursorVisible: Bool = true,
baseURL: URL? = nil
) {
self.text = text
self.showsTrailingCursor = showsTrailingCursor
self.isCursorVisible = isCursorVisible
self.baseURL = baseURL
}

public var body: some View {
Expand All @@ -30,7 +42,8 @@ public struct MarkdownContentView: View {
MarkdownUIMarkdownContentView(
text: text,
showsTrailingCursor: showsTrailingCursor,
isCursorVisible: isCursorVisible
isCursorVisible: isCursorVisible,
baseURL: baseURL
)
}
}
Expand Down Expand Up @@ -78,11 +91,13 @@ private struct MarkdownUIMarkdownContentView: View {
let text: String
let showsTrailingCursor: Bool
let isCursorVisible: Bool
let baseURL: URL?

var body: some View {
Markdown(renderedMarkdown)
Markdown(renderedMarkdown, baseURL: baseURL, imageBaseURL: baseURL)
.id(renderedMarkdown)
.markdownTheme(.rxCodeChat)
.markdownImageProvider(LocalFileImageProvider())
.markdownTextStyle(\.code) {
FontFamilyVariant(.monospaced)
FontSize(.em(0.93))
Expand Down Expand Up @@ -243,12 +258,46 @@ private func preprocessMarkdown(_ text: String) -> String {
} else if inFence {
lines.append(line)
} else {
lines.append(autoLinkURLs(sanitizeMarkdownLinkURLs(line)))
lines.append(autoLinkURLs(sanitizeMarkdownLinkURLs(convertHTMLImages(line))))
}
}
return lines.joined(separator: "\n")
}

/// Rewrites HTML `<img src="..." alt="...">` tags into markdown image syntax so
/// MarkdownUI renders them instead of treating them as raw HTML.
func convertHTMLImages(_ text: String) -> String {
let pattern = #"<img\b([^>]*?)/?>"#
guard let regex = try? NSRegularExpression(pattern: pattern, options: [.caseInsensitive]) else {
return text
}
let range = NSRange(text.startIndex..., in: text)
var result = text
for match in regex.matches(in: text, range: range).reversed() {
guard let fullRange = Range(match.range, in: result),
let attrsRange = Range(match.range(at: 1), in: result) else { continue }
let attrs = String(result[attrsRange])
guard let src = htmlAttribute("src", in: attrs) else { continue }
let alt = htmlAttribute("alt", in: attrs) ?? ""
result.replaceSubrange(fullRange, with: "![\(alt)](\(src))")
}
return result
}

private func htmlAttribute(_ name: String, in attrs: String) -> String? {
let pattern = "\(name)\\s*=\\s*(?:\"([^\"]*)\"|'([^']*)'|([^\\s>]+))"
guard let regex = try? NSRegularExpression(pattern: pattern, options: [.caseInsensitive]),
let match = regex.firstMatch(in: attrs, range: NSRange(attrs.startIndex..., in: attrs)) else {
return nil
}
for index in 1..<match.numberOfRanges {
if let range = Range(match.range(at: index), in: attrs) {
return String(attrs[range])
}
}
return nil
}

// MARK: - Textual Styles

private struct RxCodeHeadingStyle: StructuredText.HeadingStyle {
Expand Down Expand Up @@ -333,6 +382,57 @@ private struct RxCodeBlockBody: View {
}
}

// MARK: - Local File Image Provider

/// Renders images from `file://` URLs by loading them via NSImage, and falls
/// back to MarkdownUI's default network image provider for remote URLs.
private struct LocalFileImageProvider: ImageProvider {
func makeImage(url: URL?) -> some View {
Group {
if let url, url.isFileURL {
LocalFileImage(url: url)
} else {
DefaultImageProvider.default.makeImage(url: url)
}
}
}
}

private struct LocalFileImage: View {
let url: URL

var body: some View {
if let loaded = LocalFileImage.load(url: url) {
loaded.image
.resizable()
.aspectRatio(contentMode: .fit)
.frame(maxWidth: loaded.size.width)
} else {
Text("⚠︎ \(url.lastPathComponent)")
.font(.caption)
.foregroundStyle(ClaudeTheme.textTertiary)
}
}

private struct Loaded {
let image: Image
let size: CGSize
}

private static func load(url: URL) -> Loaded? {
#if canImport(AppKit)
guard let nsImage = NSImage(contentsOf: url) else { return nil }
return Loaded(image: Image(nsImage: nsImage), size: nsImage.size)
#elseif canImport(UIKit)
guard let data = try? Data(contentsOf: url),
let uiImage = UIImage(data: data) else { return nil }
return Loaded(image: Image(uiImage: uiImage), size: uiImage.size)
#else
return nil
#endif
}
}

// MARK: - Markdown Link Helpers

func sanitizeMarkdownLinkURLs(_ text: String) -> String {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -224,6 +224,9 @@
}
}
}
},
"⚠︎ %@" : {

},
"$" : {
"localizations" : {
Expand Down
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ RxCode brings Claude Code, Codex, and Agent Client Protocol (ACP) clients into o
![Platform](https://img.shields.io/badge/platform-macOS%2026.0%2B-blue)
![Swift](https://img.shields.io/badge/Swift-6.x-orange)
![License](https://img.shields.io/badge/license-Apache%202.0-green)
[![Discord](https://img.shields.io/badge/Discord-Join%20Chat-5865F2?logo=discord&logoColor=white)](https://discord.gg/ytt8SjRRNN)

<p align="center">
<img src="website/public/screenshot/screenshot1.png" alt="RxCode showing an active coding session with a diff and streaming response" width="900">
Expand Down Expand Up @@ -164,6 +165,10 @@ xcodebuild -project RxCode.xcodeproj -scheme RxCode -configuration Release build
| `website/` | Public website and screenshot assets used by this README. |
| `scripts/` | Build, notarization, Sparkle signing, and release automation. |

## Community

Join the RxCode Discord to ask questions, share feedback, and follow development: [discord.gg/ytt8SjRRNN](https://discord.gg/ytt8SjRRNN).

## License

Apache License 2.0. See [LICENSE](LICENSE) for details.
8 changes: 8 additions & 0 deletions RxCode.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -357,9 +357,17 @@
path = RxCodeUITests;
sourceTree = "<group>";
};
DF46311C2FC97C0D002D9562 /* UserMenus */ = {
isa = PBXGroup;
children = (
);
path = UserMenus;
sourceTree = "<group>";
};
E673352F2F7356F600FD26C7 = {
isa = PBXGroup;
children = (
DF46311C2FC97C0D002D9562 /* UserMenus */,
DF23F7352FB8C3EC008929A6 /* icon.icon */,
DF5B0DDC2FC023C8000CE36F /* MobileUITestPlan-iPad.xctestplan */,
DF5B0DDA2FC023BE000CE36F /* MobileUITestPlan-iPhone.xctestplan */,
Expand Down
Loading
Loading