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
25 changes: 25 additions & 0 deletions MobileUnitTestPlan.xctestplan
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"configurations" : [
{
"id" : "B1A2C3D4-0003-0003-0003-000000000003",
"name" : "Mobile Unit",
"options" : {

}
}
],
"defaultOptions" : {
"codeCoverage" : false,
"testTimeoutsEnabled" : true
},
"testTargets" : [
{
"target" : {
"containerPath" : "container:RxCode.xcodeproj",
"identifier" : "DF230B5E2FBC7368008929A6",
"name" : "RxCodeMobileTests"
}
}
],
"version" : 1
}
38 changes: 34 additions & 4 deletions Packages/Sources/MessageList/MessageList.swift
Original file line number Diff line number Diff line change
Expand Up @@ -202,15 +202,23 @@ public struct MessageList<Message: MessageListItem, RowContent: View>: View {
}

private var pinTailSpacerHeight: CGFloat {
guard pinning.pinnedUserMessageID != nil, scrollViewHeight > 0 else { return 0 }
let turnHeight = max(activeTurnMaxMeasuredHeight, rawActiveTurnMeasuredHeight)
return max(0, scrollViewHeight - turnHeight - 16)
guard pinning.isPinningUserMessage, scrollViewHeight > 0 else { return 0 }
return max(0, scrollViewHeight - activeTurnHeight - MessageListConstants.minimumPinnedTailSpacing)
}

private var rawActiveTurnMeasuredHeight: CGFloat {
max(0, tailMarkerMinY - latestUserMinY)
}

private var activeTurnHeight: CGFloat {
max(activeTurnMaxMeasuredHeight, rawActiveTurnMeasuredHeight)
}

private var pinnedTurnFillsViewport: Bool {
guard scrollViewHeight > 0 else { return false }
return activeTurnHeight >= scrollViewHeight - MessageListConstants.minimumPinnedTailSpacing
}

private var isAnchoredAtBottom: Bool {
anchor.isNearBottom && isAtBottom
}
Expand All @@ -223,6 +231,22 @@ public struct MessageList<Message: MessageListItem, RowContent: View>: View {
messages.last { $0.isUserMessage }?.id
}

private var hasContentAfterPinnedUserMessage: Bool {
guard let pinnedID = pinning.pinnedUserMessageID,
let pinnedIndex = messages.firstIndex(where: { $0.id == pinnedID })
else { return false }

let nextIndex = messages.index(after: pinnedIndex)
guard nextIndex < messages.endIndex else { return false }
return messages[nextIndex...].contains { !$0.isMessageListAccessory }
}

private var shouldReleasePinnedUserMessageForFilledTurn: Bool {
pinning.isPinningUserMessage
&& hasContentAfterPinnedUserMessage
&& pinnedTurnFillsViewport
}

private var messageListChangeToken: MessageListChangeToken<Message.ID> {
MessageListChangeToken(
ids: messages.map(\.id),
Expand All @@ -237,6 +261,11 @@ public struct MessageList<Message: MessageListItem, RowContent: View>: View {
visibleMaxY: metrics.visibleMaxY
)
updateIsAtBottomBinding(anchor.isNearBottom)

if shouldReleasePinnedUserMessageForFilledTurn, !isUserDrivenScroll {
releasePinnedUserMessage(proxy: proxy)
}

if decision == .scrollToBottom,
isAtBottom,
isStreaming,
Expand Down Expand Up @@ -426,7 +455,7 @@ public struct MessageList<Message: MessageListItem, RowContent: View>: View {
}

private func updateActiveTurnMaxMeasuredHeight() {
guard pinning.pinnedUserMessageID != nil else { return }
guard pinning.isPinningUserMessage else { return }
let measured = rawActiveTurnMeasuredHeight
guard measured > activeTurnMaxMeasuredHeight + 0.5 else { return }
var transaction = Transaction()
Expand Down Expand Up @@ -492,6 +521,7 @@ private nonisolated enum MessageListConstants {
static let tailMarkerID = "message-list-tail-marker"
static let coordinateSpaceName = "message-list-content"
static let loadThreshold: CGFloat = 96
static let minimumPinnedTailSpacing: CGFloat = 16
static let userScrollDownDelta: CGFloat = 4
static let layoutSettleDelayNanoseconds: UInt64 = 16_000_000
static let streamingBottomScrollInterval: TimeInterval = 2
Expand Down
38 changes: 27 additions & 11 deletions Packages/Sources/RxCodeChatKit/ChangeDiffView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,9 @@ public struct ChangeDiffView: View {
@ViewBuilder
private func unifiedRows(_ diff: String) -> some View {
let lines = diff.components(separatedBy: .newlines)
ForEach(Array(lines.enumerated()), id: \.offset) { _, line in
ForEach(Array(lines.enumerated()), id: \.offset) { index, line in
diffRow(
lineNumber: unifiedLineNumber(line: line, offset: index),
text: line.isEmpty ? " " : line,
color: unifiedColor(line),
background: unifiedBackground(line)
Expand All @@ -66,27 +67,42 @@ public struct ChangeDiffView: View {
let added = hunk.newString
.components(separatedBy: .newlines)
.map { ("+ " + $0, ClaudeTheme.statusSuccess, ClaudeTheme.statusSuccess.opacity(0.06)) }
ForEach(Array((removed + added).enumerated()), id: \.offset) { _, item in
diffRow(text: item.0, color: item.1, background: item.2)
ForEach(Array((removed + added).enumerated()), id: \.offset) { offset, item in
diffRow(lineNumber: offset + 1, text: item.0, color: item.1, background: item.2)
}
}
}

// MARK: - Shared row

private func diffRow(text: String, color: Color, background: Color) -> some View {
ChatTextContentView(
text,
size: ClaudeTheme.messageSize(12),
design: .monospaced,
color: color
)
.frame(maxWidth: .infinity, alignment: .leading)
private func diffRow(lineNumber: Int? = nil, text: String, color: Color, background: Color) -> some View {
HStack(alignment: .firstTextBaseline, spacing: 8) {
Text(lineNumber.map(String.init) ?? "")
.font(.system(size: ClaudeTheme.messageSize(11), design: .monospaced))
.foregroundStyle(ClaudeTheme.textTertiary)
.frame(width: 34, alignment: .trailing)
.accessibilityIdentifier("diff-line-number")

ChatTextContentView(
text,
size: ClaudeTheme.messageSize(12),
design: .monospaced,
color: color
)
.frame(maxWidth: .infinity, alignment: .leading)
}
.padding(.horizontal, 8)
.padding(.vertical, 1)
.background(background)
}

private func unifiedLineNumber(line: String, offset: Int) -> Int? {
if line.hasPrefix("diff ") || line.hasPrefix("index ") || line.hasPrefix("---") || line.hasPrefix("+++") || line.hasPrefix("@@") {
return nil
}
return offset + 1
}

private func unifiedColor(_ line: String) -> Color {
if line.hasPrefix("+"), !line.hasPrefix("+++") { return ClaudeTheme.statusSuccess }
if line.hasPrefix("-"), !line.hasPrefix("---") { return ClaudeTheme.statusError }
Expand Down
Loading
Loading