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
20 changes: 16 additions & 4 deletions clients/macos/vellum-assistant/Features/Chat/UsedToolsList.swift
Original file line number Diff line number Diff line change
Expand Up @@ -82,14 +82,15 @@ private struct UsedToolsRow: View {
@State private var isExpanded = false
@State private var isHovered = false
@State private var isImageHovered = false
/// Cached formatted input — computed once on first expand to avoid re-running
/// `formatAllToolInput` on every SwiftUI render pass.
@State private var cachedInputFull: String?
@Environment(\.displayScale) private var displayScale

/// Lazily resolved full input text. If `inputFull` was deferred during history
/// load, compute it from the raw dictionary on first access (when the user
/// expands the row) instead of during `populateFromHistory`.
/// Lazily resolved full input text, using the cached value when available.
private var resolvedInputFull: String {
if let cached = cachedInputFull { return cached }
if !toolCall.inputFull.isEmpty { return toolCall.inputFull }
if let dict = toolCall.inputRawDict { return ToolCallData.formatAllToolInput(dict) }
return ""
}

Expand Down Expand Up @@ -276,6 +277,17 @@ private struct UsedToolsRow: View {
}
.padding(.bottom, VSpacing.sm)
.transition(.opacity.combined(with: .move(edge: .top)))
.onAppear {
// Compute formatted input once when the user first expands,
// rather than re-running formatAllToolInput on every render.
if cachedInputFull == nil {
if !toolCall.inputFull.isEmpty {
cachedInputFull = toolCall.inputFull
} else if let dict = toolCall.inputRawDict {
cachedInputFull = ToolCallData.formatAllToolInput(dict)
}
}
}
}
}
.animation(VAnimation.fast, value: isExpanded)
Expand Down
8 changes: 0 additions & 8 deletions clients/shared/Features/Chat/ChatMessage.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1023,14 +1023,6 @@ public struct ToolCallData: Identifiable, Equatable {
return result
}

/// If `inputFull` is empty and `inputRawDict` is available, compute the
/// formatted input on demand. Call this before displaying `inputFull`.
public mutating func ensureInputFullFormatted() {
guard inputFull.isEmpty, let dict = inputRawDict else { return }
inputFull = Self.formatAllToolInput(dict)
inputRawDict = nil
}

private static func stringifyValue(_ value: AnyCodable) -> String {
if let s = value.value as? String { return s }
if let b = value.value as? Bool { return b ? "true" : "false" }
Expand Down
20 changes: 18 additions & 2 deletions clients/shared/Features/Chat/ChatViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1738,8 +1738,24 @@ public final class ChatViewModel: ObservableObject {
imageData: tc.imageData
)
// Defer expensive formatting — store the raw dict for lazy computation
// when the user expands the tool call chip.
toolCall.inputRawDict = tc.input
// when the user expands the tool call chip. Cap the raw dict size
// to prevent unbounded memory from large tool inputs (mirrors the
// 10k-char cap applied in formatAllToolInput).
if let input = tc.input {
let estimatedSize: Int
if let data = try? JSONSerialization.data(withJSONObject: input.mapValues { $0.value ?? NSNull() }) {
estimatedSize = data.count
} else {
estimatedSize = 0
}
if estimatedSize > 10_000 {
// Too large — format eagerly (with truncation) rather than
// retaining the full raw dictionary in memory.
toolCall.inputFull = ToolCallData.formatAllToolInput(input)
} else {
toolCall.inputRawDict = input
}
}
return toolCall
}
}
Expand Down
20 changes: 16 additions & 4 deletions clients/shared/Features/Chat/ToolCallChip.swift
Original file line number Diff line number Diff line change
Expand Up @@ -10,17 +10,18 @@ public struct ToolCallChip: View {
self.toolCall = toolCall
}
@State private var isExpanded = false
/// Cached formatted input — computed once on first expand to avoid re-running
/// `formatAllToolInput` on every SwiftUI render pass.
@State private var cachedInputFull: String?

private var hasExpandableContent: Bool {
toolCall.result != nil || toolCall.cachedImage != nil
}

/// Lazily resolved full input text. If `inputFull` was deferred during history
/// load, compute it from the raw dictionary on first access (when the user
/// expands the chip) instead of during `populateFromHistory`.
/// Lazily resolved full input text, using the cached value when available.
private var resolvedInputFull: String {
if let cached = cachedInputFull { return cached }
if !toolCall.inputFull.isEmpty { return toolCall.inputFull }
if let dict = toolCall.inputRawDict { return ToolCallData.formatAllToolInput(dict) }
return ""
}
Comment thread
ashleeradka marked this conversation as resolved.

Expand Down Expand Up @@ -148,6 +149,17 @@ public struct ToolCallChip: View {
}
}
.padding(.bottom, VSpacing.sm)
.onAppear {
// Compute formatted input once when the user first expands,
// rather than re-running formatAllToolInput on every render.
if cachedInputFull == nil {
if !toolCall.inputFull.isEmpty {
cachedInputFull = toolCall.inputFull
} else if let dict = toolCall.inputRawDict {
cachedInputFull = ToolCallData.formatAllToolInput(dict)
}
}
}
}
}
.background(
Expand Down
Loading