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
35 changes: 34 additions & 1 deletion Packages/Sources/RxCodeChatKit/MarkdownView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -34,7 +37,7 @@ struct MarkdownContentView: View {
)
.textual.headingStyle(RxCodeHeadingStyle())
.textual.codeBlockStyle(RxCodeBlockStyle())
.textual.textSelection(.enabled)
.markdownTextSelection(enabled: !isScrollActive)
.frame(maxWidth: .infinity, alignment: .leading)
}

Expand All @@ -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
Expand Down
9 changes: 9 additions & 0 deletions Packages/Sources/RxCodeChatKit/MessageListView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ struct MessageListView: View {
@State private var readyTask: Task<Void, Never>?
@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"
Expand Down Expand Up @@ -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
Expand All @@ -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 ?? "<nil>"
Self.log.info("[MessageList.task] fired sid=\(sid, privacy: .public) bridgeMessages=\(chatBridge.messages.count) isStreaming=\(chatBridge.isStreaming) isLoadingFromDisk=\(chatBridge.isLoadingFromDisk)")
Expand Down
1 change: 0 additions & 1 deletion RxCodeMobile/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
<key>UIBackgroundModes</key>
<array>
<string>remote-notification</string>
<string>processing</string>
</array>
<key>UILaunchScreen</key>
<dict/>
Expand Down
47 changes: 47 additions & 0 deletions ci_scripts/ci_post_clone.sh
Original file line number Diff line number Diff line change
@@ -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.
Comment on lines +3 to +9

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"

Comment on lines +28 to +30
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"
57 changes: 57 additions & 0 deletions website/app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
}
25 changes: 18 additions & 7 deletions website/app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down Expand Up @@ -422,14 +432,14 @@ function MobileCompanion() {
id="mobile"
className="max-w-[var(--container-max)] mx-auto px-6 pb-24"
>
<div className="flex items-center gap-3 mb-8">
<div className="flex items-center gap-3 mb-8 mobile-rise">
<span className="font-mono text-[11px] tracking-widest uppercase text-primary">
Mobile Companion
</span>
<span className="flex-1 h-px bg-surface-variant" />
</div>
<div className="grid grid-cols-1 lg:grid-cols-[1fr_1.35fr] gap-12 lg:gap-16 items-center">
<div>
<div className="mobile-rise">
<div className="flex items-center gap-2 mb-4 font-mono text-[11px] tracking-widest uppercase text-primary">
<LockIcon className="w-3.5 h-3.5" />
End-to-end encrypted sync
Expand All @@ -439,7 +449,8 @@ function MobileCompanion() {
</h2>
<p className="text-on-surface-variant leading-relaxed mb-7">
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.
</p>
Expand Down Expand Up @@ -474,18 +485,18 @@ function MobileCompanion() {
</div>
<div className="grid grid-cols-2 gap-4 sm:gap-5">
{MOBILE_SHOTS.map((shot) => (
<figure key={shot.image} className="flex flex-col">
<div className="bg-surface-container border border-surface-variant relative overflow-hidden">
<figure key={shot.image} className="group flex flex-col mobile-shot">
<div className="bg-surface-container border border-surface-variant relative overflow-hidden transition-[transform,border-color,box-shadow] duration-300 ease-out group-hover:-translate-y-2 group-hover:border-primary/60 group-hover:shadow-[0_18px_44px_-22px_rgba(0,0,0,0.75)]">
<Image
src={shot.image}
alt={shot.alt}
width={1320}
height={2868}
sizes="(min-width: 1024px) 330px, (min-width: 640px) 45vw, 45vw"
className="block w-full h-auto"
className="block w-full h-auto transition-transform duration-500 ease-out group-hover:scale-[1.04]"
/>
</div>
<figcaption className="mt-3 font-mono text-[10px] tracking-widest uppercase text-on-surface-variant/70">
<figcaption className="mt-3 font-mono text-[10px] tracking-widest uppercase text-on-surface-variant/70 transition-colors duration-300 group-hover:text-primary">
{shot.caption}
</figcaption>
</figure>
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.