Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
a5c225b
Fix adaptive scroll frame performance: char-count safety net, byte-sc…
devin-ai-integration[bot] Apr 7, 2026
0ed8d4d
Wire cached line counts into adaptiveScrollFrame to eliminate dead state
devin-ai-integration[bot] Apr 7, 2026
57b2416
Fix ViewBuilder error: replace for loop with reduce for line counting
devin-ai-integration[bot] Apr 7, 2026
a6faee9
Remove shared cachedPreviewLineCount from ToolConfirmationBubble
devin-ai-integration[bot] Apr 7, 2026
5d25a53
Short-circuit text.count in adaptiveScrollFrame, fix stale cache on s…
devin-ai-integration[bot] Apr 7, 2026
7ddeadc
Fix inconsistent char threshold guard in AssistantProgressView.output…
devin-ai-integration[bot] Apr 7, 2026
8da3ace
Skip coloredOutput for long content: avoid O(n) AttributedString on e…
devin-ai-integration[bot] Apr 7, 2026
989c4b4
Add cache invalidation for result changes in ToolCallProgressBar
devin-ai-integration[bot] Apr 7, 2026
970d9d2
Simplify outputBlock: always use maxHeight, cache coloredOutput via @…
devin-ai-integration[bot] Apr 7, 2026
96f301e
Remove unused cachedResultString @State property
devin-ai-integration[bot] Apr 7, 2026
0c421f3
Restore definite-height branching in outputBlock, bound resolvedInput…
devin-ai-integration[bot] Apr 7, 2026
47c0257
Clean up comments: remove iteration-specific language from docstrings
devin-ai-integration[bot] Apr 7, 2026
b8d836a
Fix short-content height regression: align lineThreshold with maxHeig…
devin-ai-integration[bot] Apr 7, 2026
f0b3ec0
Fix review feedback: utf8.count, cached input sizing, eager coloredOu…
devin-ai-integration[bot] Apr 7, 2026
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
Original file line number Diff line number Diff line change
Expand Up @@ -1012,11 +1012,12 @@ private struct StepDetailRow: View {
copyText: String,
copyLabel: String
) -> some View {
let lineCount = copyText.components(separatedBy: "\n").count
let lineCount = copyText.utf8.reduce(1) { count, byte in byte == 0x0A ? count + 1 : count }
let isLong = lineCount > 500 || copyText.count > 50_000
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Outdated

ZStack(alignment: .topTrailing) {
VStack(alignment: .leading, spacing: VSpacing.xs) {
if lineCount > 500 {
if isLong {
// Content at 500+ lines always exceeds 400pt, so a fixed
// height lets sizeThatFits return without measuring content.
ScrollView {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,21 +3,30 @@ import SwiftUI
extension View {
/// Applies an adaptive height constraint to a `ScrollView` inside a `LazyVStack` cell.
///
/// For content exceeding `lineThreshold` lines, a definite `frame(height:)` is used so
/// `LazyVStack` can skip scroll-content measurement during cell sizing. For shorter content
/// `frame(maxHeight:)` is used so the view collapses to its natural height instead of
/// rendering with blank space.
/// For content exceeding `lineThreshold` lines **or** `charThreshold` characters, a definite
/// `frame(height:)` is used so `LazyVStack` can skip scroll-content measurement during cell
/// sizing. The character threshold catches single-line mega-strings (e.g. base64 data,
/// minified JSON) that would otherwise trigger an expensive Core Text width measurement.
/// For shorter content `frame(maxHeight:)` is used so the view collapses to its natural
/// height instead of rendering with blank space.
///
/// - Parameters:
/// - text: The string whose line count determines which constraint is applied.
/// - text: The string whose size determines which constraint is applied.
/// - maxHeight: The height cap applied in both branches.
/// - lineThreshold: Line count above which the fixed height is used. Default: 500.
/// - charThreshold: Character count above which the fixed height is used. Default: 50 000.
/// - lineCount: Pre-computed line count. When provided, the modifier skips its
/// internal `countLines` scan. Use this when the caller caches the line count
/// via `@State` to avoid redundant O(n) work on re-render.
func adaptiveScrollFrame(
for text: String,
maxHeight: CGFloat,
lineThreshold: Int = 500
lineThreshold: Int = 500,
charThreshold: Int = 50_000,
lineCount: Int? = nil
) -> some View {
let isLong = countLines(in: text) > lineThreshold
let lines = lineCount ?? countLines(in: text)
let isLong = lines > lineThreshold || text.count > charThreshold
return self
.frame(height: isLong ? maxHeight : nil)
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
.frame(maxHeight: isLong ? nil : maxHeight)
Expand Down
2 changes: 1 addition & 1 deletion clients/shared/Features/Chat/ToolCallChip.swift
Original file line number Diff line number Diff line change
Expand Up @@ -237,7 +237,7 @@ public struct ToolCallChip: View {
.frame(maxWidth: .infinity, alignment: .leading)
.textSelection(.enabled)
}
.adaptiveScrollFrame(for: result, maxHeight: 400)
.adaptiveScrollFrame(for: result, maxHeight: 400, lineCount: lineCount)
}
}
}
Expand Down
22 changes: 21 additions & 1 deletion clients/shared/Features/Chat/ToolCallProgressBar.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,9 @@ import SwiftUI
public struct ToolCallProgressBar: View {
public let toolCalls: [ToolCallData]
@State private var expandedStepId: UUID?
/// Cached line count for the expanded tool call's result text — avoids O(n)
/// byte scan on every SwiftUI render pass when a step is expanded.
@State private var cachedResultLineCount: Int?
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.

public init(toolCalls: [ToolCallData]) {
self.toolCalls = toolCalls
Expand Down Expand Up @@ -250,12 +253,29 @@ public struct ToolCallProgressBar: View {
.frame(maxWidth: .infinity, alignment: .leading)
.textSelection(.enabled)
}
.adaptiveScrollFrame(for: result, maxHeight: 200)
.adaptiveScrollFrame(for: result, maxHeight: 200, lineThreshold: 200, lineCount: cachedResultLineCount)
Comment thread
ashleeradka marked this conversation as resolved.
Outdated
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
Outdated
}
}
}
}
.padding(VSpacing.md)
.onAppear {
if cachedResultLineCount == nil,
let expandedId = expandedStepId,
let expandedCall = toolCalls.first(where: { $0.id == expandedId }),
let result = expandedCall.result {
cachedResultLineCount = ToolCallChip.countLines(in: result)
Comment thread
ashleeradka marked this conversation as resolved.
}
}
.onChange(of: expandedStepId) {
if let expandedId = expandedStepId,
let expandedCall = toolCalls.first(where: { $0.id == expandedId }),
let result = expandedCall.result {
cachedResultLineCount = ToolCallChip.countLines(in: result)
} else {
cachedResultLineCount = nil
}
}
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.
.background(
RoundedRectangle(cornerRadius: VRadius.md)
.fill(VColor.surfaceOverlay)
Expand Down