diff --git a/Packages/Sources/RxCodeChatKit/MarkdownView.swift b/Packages/Sources/RxCodeChatKit/MarkdownView.swift index c8eae38..2b2e331 100644 --- a/Packages/Sources/RxCodeChatKit/MarkdownView.swift +++ b/Packages/Sources/RxCodeChatKit/MarkdownView.swift @@ -12,6 +12,9 @@ struct MarkdownContentView: View { let showsTrailingCursor: Bool let isCursorVisible: Bool + /// `true` while the chat list is scrolling — see `markdownTextSelection`. + @Environment(\.chatListScrollActive) private var isScrollActive + init(text: String, showsTrailingCursor: Bool = false, isCursorVisible: Bool = true) { self.text = text self.showsTrailingCursor = showsTrailingCursor @@ -34,7 +37,7 @@ struct MarkdownContentView: View { ) .textual.headingStyle(RxCodeHeadingStyle()) .textual.codeBlockStyle(RxCodeBlockStyle()) - .textual.textSelection(.enabled) + .markdownTextSelection(enabled: !isScrollActive) .frame(maxWidth: .infinity, alignment: .leading) } @@ -49,6 +52,36 @@ struct MarkdownContentView: View { } } +// MARK: - Text Selection Toggle + +extension EnvironmentValues { + /// `true` while the chat message list is actively scrolling. + /// + /// Markdown rows read this to drop Textual's text-selection overlay + /// mid-scroll — see `View.markdownTextSelection(enabled:)`. + @Entry var chatListScrollActive: Bool = false +} + +private extension View { + /// Applies Textual text selection, gated by `enabled`. + /// + /// Textual's selection overlay installs a per-message `Text.LayoutKey` + /// preference observer whose `onChange` mutates an `@Observable` model on + /// every layout pass. While a `List` scrolls, that fires repeatedly within + /// a single frame ("onChange(of: AnyTextLayoutCollection) ... tried to + /// update multiple times per frame"), dropping frames and making the + /// scroll bumpy. Suspending selection during scroll removes the overlay + /// entirely; it is restored the instant the list settles. + @ViewBuilder + func markdownTextSelection(enabled: Bool) -> some View { + if enabled { + textual.textSelection(.enabled) + } else { + textual.textSelection(.disabled) + } + } +} + // MARK: - Markdown Preprocessing /// Applies bare-URL auto-linking and link sanitization, skipping fenced code blocks diff --git a/Packages/Sources/RxCodeChatKit/MessageListView.swift b/Packages/Sources/RxCodeChatKit/MessageListView.swift index 21af677..ea9b55d 100644 --- a/Packages/Sources/RxCodeChatKit/MessageListView.swift +++ b/Packages/Sources/RxCodeChatKit/MessageListView.swift @@ -17,6 +17,9 @@ struct MessageListView: View { @State private var readyTask: Task? @State private var anchor = AutoScrollAnchor() @State private var isSessionReady = false + /// Tracks active scrolling so markdown rows can suspend Textual's + /// text-selection overlay mid-scroll (avoids per-frame layout cycles). + @State private var isScrollActive = false private static let log = Logger(subsystem: "com.claudework", category: "MessageListView") private static let bottomAnchorID = "message-list-bottom-anchor" @@ -68,6 +71,7 @@ struct MessageListView: View { .contentMargins(.top, 16, for: .scrollContent) .scrollContentBackground(.hidden) .environment(\.defaultMinListRowHeight, 0) + .environment(\.chatListScrollActive, isScrollActive) .opacity(isSessionReady ? 1 : 0) .defaultScrollAnchor(.bottom) .onScrollGeometryChange(for: ScrollSample.self) { geo in @@ -82,6 +86,11 @@ struct MessageListView: View { scrollToBottomDebounced(proxy) } } + .onScrollPhaseChange { _, newPhase in + // Suspend Textual text selection while the list is in motion and + // restore it the instant scrolling settles back to `.idle`. + isScrollActive = newPhase != .idle + } .task(id: windowState.currentSessionId) { let sid = windowState.currentSessionId ?? "" Self.log.info("[MessageList.task] fired sid=\(sid, privacy: .public) bridgeMessages=\(chatBridge.messages.count) isStreaming=\(chatBridge.isStreaming) isLoadingFromDisk=\(chatBridge.isLoadingFromDisk)") diff --git a/RxCodeMobile/Info.plist b/RxCodeMobile/Info.plist index e34d152..0a6c729 100644 --- a/RxCodeMobile/Info.plist +++ b/RxCodeMobile/Info.plist @@ -19,7 +19,6 @@ UIBackgroundModes remote-notification - processing UILaunchScreen diff --git a/ci_scripts/ci_post_clone.sh b/ci_scripts/ci_post_clone.sh new file mode 100755 index 0000000..9405ab1 --- /dev/null +++ b/ci_scripts/ci_post_clone.sh @@ -0,0 +1,47 @@ +#!/bin/bash + +# Xcode Cloud post-clone hook for RxCodeMobile +# - Stamps the app version from the git tag (CI_TAG -> MARKETING_VERSION) +# - Sets the build number from CI_BUILD_NUMBER (CURRENT_PROJECT_VERSION) +# +# Xcode Cloud runs this script automatically right after cloning the repo. +# Configure the Xcode Cloud workflow to start a build on new tag creation; +# CI_TAG and CI_BUILD_NUMBER are then provided by Xcode Cloud. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" +PROJECT_DIR="$REPO_ROOT" +PROJECT_FILE="$REPO_ROOT/RxCode.xcodeproj/project.pbxproj" + +echo "== Xcode Cloud: ci_post_clone ==" +echo "Repo root: $REPO_ROOT" + +# Xcode plugin/macro validation often blocks CI for SPM dependency plugins. +defaults write com.apple.dt.Xcode IDESkipPackagePluginFingerprintValidatation -bool YES || true +defaults write com.apple.dt.Xcode IDESkipMacroFingerprintValidation -bool YES || true + +if [[ -n "${CI_TAG:-}" ]]; then + VERSION="${CI_TAG#v}" + if [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+([-.][A-Za-z0-9.]+)?$ ]]; then + echo "Stamping MARKETING_VERSION from CI_TAG: $CI_TAG -> $VERSION" + sed -i '' "s/MARKETING_VERSION = [^;]*;/MARKETING_VERSION = $VERSION;/g" "$PROJECT_FILE" + + if [[ -n "${CI_BUILD_NUMBER:-}" ]]; then + echo "Stamping CURRENT_PROJECT_VERSION from CI_BUILD_NUMBER: $CI_BUILD_NUMBER" + ( + cd "$PROJECT_DIR" + xcrun agvtool new-version -all "$CI_BUILD_NUMBER" + ) + else + echo "CI_BUILD_NUMBER not set; skipping CURRENT_PROJECT_VERSION update" + fi + else + echo "CI_TAG '$CI_TAG' is not a semver tag; skipping version stamping" + fi +else + echo "CI_TAG not set; skipping version stamping" +fi + +echo "ci_post_clone completed" diff --git a/website/app/globals.css b/website/app/globals.css index 00f1b84..6d03c9e 100644 --- a/website/app/globals.css +++ b/website/app/globals.css @@ -302,3 +302,60 @@ body { opacity: 0.85; } } + +/* --- Mobile companion section --- */ +@keyframes mobile-shot-reveal { + from { + opacity: 0; + transform: translateY(38px) scale(0.96); + } + to { + opacity: 1; + transform: translateY(0) scale(1); + } +} + +@keyframes mobile-rise { + from { + opacity: 0; + transform: translateY(24px); + } + to { + opacity: 1; + transform: translateY(0); + } +} + +/* Phone screenshots reveal with a staggered lift as they scroll into view. */ +.mobile-shot { + animation: mobile-shot-reveal both linear; + animation-timeline: view(); + animation-range: entry 5% cover 28%; +} + +/* Offset the right column so each row reveals left-to-right. */ +.mobile-shot:nth-child(even) { + animation-range: entry 13% cover 36%; +} + +/* Section header and copy fade up on entry. */ +.mobile-rise { + animation: mobile-rise both linear; + animation-timeline: view(); + animation-range: entry 0% cover 20%; +} + +/* Browsers without scroll-driven animations show everything at rest. */ +@supports not (animation-timeline: view()) { + .mobile-shot, + .mobile-rise { + animation: none; + } +} + +@media (prefers-reduced-motion: reduce) { + .mobile-shot, + .mobile-rise { + animation: none; + } +} diff --git a/website/app/page.tsx b/website/app/page.tsx index c8fe3cd..1946fb5 100644 --- a/website/app/page.tsx +++ b/website/app/page.tsx @@ -117,6 +117,16 @@ const MOBILE_SHOTS = [ alt: "RxCode Mobile following a live agent conversation", caption: "Follow live sessions", }, + { + image: "/screenshot/mobile-screenshot-6.png", + alt: "RxCode Mobile previewing a website served from the paired desktop dev server in the in-app browser", + caption: "Preview your dev server", + }, + { + image: "/screenshot/mobile-screenshot-5.PNG", + alt: "RxCode Mobile showing an agent briefing that summarizes the work an agent completed", + caption: "Read agent briefings", + }, ]; const MOBILE_POINTS = [ @@ -422,14 +432,14 @@ function MobileCompanion() { id="mobile" className="max-w-[var(--container-max)] mx-auto px-6 pb-24" > -
+
Mobile Companion
-
+
End-to-end encrypted sync @@ -439,7 +449,8 @@ function MobileCompanion() {

The RxCode companion app mirrors your Mac. Browse projects, search - every thread, kick off new agent runs, and follow live sessions from + every thread, kick off new agent runs, follow live sessions, preview + your dev server in the browser, and catch up on agent briefings from your phone — all relayed over an end-to-end encrypted channel so nothing in the middle can read your code or conversations.

@@ -474,18 +485,18 @@ function MobileCompanion() {
{MOBILE_SHOTS.map((shot) => ( -
-
+
+
{shot.alt}
-
+
{shot.caption}
diff --git a/website/public/screenshot/mobile-screenshot-5.PNG b/website/public/screenshot/mobile-screenshot-5.PNG new file mode 100644 index 0000000..96b591b Binary files /dev/null and b/website/public/screenshot/mobile-screenshot-5.PNG differ diff --git a/website/public/screenshot/mobile-screenshot-6.png b/website/public/screenshot/mobile-screenshot-6.png new file mode 100644 index 0000000..29c40a9 Binary files /dev/null and b/website/public/screenshot/mobile-screenshot-6.png differ