Skip to content

Commit 54b0ae2

Browse files
mcintyre94claude
andcommitted
Fix chat input height for programmatic text insertion and code review improvements
- Replace manual dynamicHeight binding with UIViewRepresentable.sizeThatFits so SwiftUI uses the correct proposed width (not frame.width which can be zero during sheet dismissal), fixing overflow when inserting bash output into chat - Call invalidateIntrinsicContentSize in updateUIView to trigger a second layout pass after programmatic text changes - Make maxHeight/minHeight static constants - Guard against nil chatViewModel in startChatCallback to avoid silent text drop - Add previews for Insert/StartChat paths in BashQuickView and QuickActionsView Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent c344ea1 commit 54b0ae2

File tree

5 files changed

+69
-23
lines changed

5 files changed

+69
-23
lines changed

Wisp/Views/QuickActions/BashQuickView.swift

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ struct BashQuickView: View {
133133
}
134134
}
135135

136-
#Preview {
136+
#Preview("No callback") {
137137
BashQuickView(
138138
viewModel: BashQuickViewModel(
139139
spriteName: "my-sprite",
@@ -142,3 +142,25 @@ struct BashQuickView: View {
142142
)
143143
.environment(SpritesAPIClient())
144144
}
145+
146+
#Preview("Insert into chat") {
147+
BashQuickView(
148+
viewModel: BashQuickViewModel(
149+
spriteName: "my-sprite",
150+
workingDirectory: "/home/sprite/project"
151+
),
152+
onInsert: { _ in }
153+
)
154+
.environment(SpritesAPIClient())
155+
}
156+
157+
#Preview("Start Chat") {
158+
BashQuickView(
159+
viewModel: BashQuickViewModel(
160+
spriteName: "my-sprite",
161+
workingDirectory: "/home/sprite/project"
162+
),
163+
onStartChat: { _ in }
164+
)
165+
.environment(SpritesAPIClient())
166+
}

Wisp/Views/QuickActions/QuickActionsView.swift

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ struct QuickActionsView: View {
6565
}
6666
}
6767

68-
#Preview {
68+
#Preview("No callback") {
6969
QuickActionsView(
7070
viewModel: QuickActionsViewModel(
7171
spriteName: "my-sprite",
@@ -75,3 +75,27 @@ struct QuickActionsView: View {
7575
)
7676
.environment(SpritesAPIClient())
7777
}
78+
79+
#Preview("Insert into chat") {
80+
QuickActionsView(
81+
viewModel: QuickActionsViewModel(
82+
spriteName: "my-sprite",
83+
sessionId: nil,
84+
workingDirectory: "/home/sprite/project"
85+
),
86+
insertCallback: { _ in }
87+
)
88+
.environment(SpritesAPIClient())
89+
}
90+
91+
#Preview("Start Chat") {
92+
QuickActionsView(
93+
viewModel: QuickActionsViewModel(
94+
spriteName: "my-sprite",
95+
sessionId: nil,
96+
workingDirectory: "/home/sprite/project"
97+
),
98+
startChatCallback: { _ in }
99+
)
100+
.environment(SpritesAPIClient())
101+
}

Wisp/Views/SpriteDetail/Chat/ChatInputBar.swift

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,6 @@ struct ChatInputBar: View {
2020
var isFocused: FocusState<Bool>.Binding
2121

2222
@State private var showStopConfirmation = false
23-
@State private var textInputHeight: CGFloat = 36
2423

2524
private var isEmpty: Bool {
2625
text.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty && attachedFiles.isEmpty
@@ -75,10 +74,8 @@ struct ChatInputBar: View {
7574
isFocused: isFocused,
7675
isDisabled: hasQueuedMessage,
7776
placeholder: "Message...",
78-
onPasteNonText: onPasteFromClipboard,
79-
dynamicHeight: $textInputHeight
77+
onPasteNonText: onPasteFromClipboard
8078
)
81-
.frame(height: max(textInputHeight, 36))
8279
.padding(.horizontal, 16)
8380
.padding(.vertical, 8)
8481
.glassEffect(in: .rect(cornerRadius: 20))
@@ -119,9 +116,6 @@ struct ChatInputBar: View {
119116
.buttonStyle(.glass)
120117
}
121118
}
122-
.onChange(of: text) { _, newValue in
123-
if newValue.isEmpty { textInputHeight = 36 }
124-
}
125119
.animation(.easeInOut(duration: 0.2), value: attachedFiles.count)
126120
.animation(.easeInOut(duration: 0.2), value: lastUploadedFileName)
127121
.padding(.horizontal)

Wisp/Views/SpriteDetail/Chat/PasteInterceptingTextInput.swift

Lines changed: 18 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,9 @@ struct PasteInterceptingTextInput: UIViewRepresentable {
8484
var isDisabled: Bool
8585
var placeholder: String
8686
var onPasteNonText: (() -> Void)?
87-
@Binding var dynamicHeight: CGFloat
87+
88+
private static let maxHeight: CGFloat = 120
89+
private static let minHeight: CGFloat = 36
8890

8991
func makeUIView(context: Context) -> PasteTextView {
9092
let textView = PasteTextView()
@@ -100,10 +102,22 @@ struct PasteInterceptingTextInput: UIViewRepresentable {
100102
return textView
101103
}
102104

105+
func sizeThatFits(_ proposal: ProposedViewSize, uiView: PasteTextView, context: Context) -> CGSize? {
106+
let width = proposal.width ?? uiView.frame.width
107+
guard width > 0 else { return nil }
108+
let fits = uiView.sizeThatFits(CGSize(width: width, height: .infinity))
109+
let height = max(min(fits.height, Self.maxHeight), Self.minHeight)
110+
uiView.isScrollEnabled = fits.height > Self.maxHeight
111+
return CGSize(width: width, height: height)
112+
}
113+
103114
func updateUIView(_ textView: PasteTextView, context: Context) {
115+
context.coordinator.parent = self
104116
if textView.text != text {
105117
textView.text = text
106118
textView.updatePlaceholderVisibility()
119+
// Invalidate so SwiftUI calls sizeThatFits again with the updated text
120+
textView.invalidateIntrinsicContentSize()
107121
}
108122
textView.isUserInteractionEnabled = !isDisabled
109123
textView.placeholder = placeholder
@@ -128,7 +142,6 @@ struct PasteInterceptingTextInput: UIViewRepresentable {
128142
final class Coordinator: NSObject, UITextViewDelegate {
129143
var parent: PasteInterceptingTextInput
130144
var isEditing = false
131-
private let maxHeight: CGFloat = 120
132145

133146
init(_ parent: PasteInterceptingTextInput) {
134147
self.parent = parent
@@ -137,13 +150,8 @@ struct PasteInterceptingTextInput: UIViewRepresentable {
137150
func textViewDidChange(_ textView: UITextView) {
138151
parent.text = textView.text
139152
(textView as? PasteTextView)?.updatePlaceholderVisibility()
140-
141-
let fitsSize = textView.sizeThatFits(CGSize(width: textView.frame.width, height: .infinity))
142-
let newHeight = min(fitsSize.height, maxHeight)
143-
if newHeight != parent.dynamicHeight {
144-
parent.dynamicHeight = newHeight
145-
}
146-
textView.isScrollEnabled = fitsSize.height > maxHeight
153+
// Signal SwiftUI to re-query sizeThatFits with the new content
154+
textView.invalidateIntrinsicContentSize()
147155
}
148156

149157
func textViewDidBeginEditing(_ textView: UITextView) {
@@ -160,16 +168,13 @@ struct PasteInterceptingTextInput: UIViewRepresentable {
160168

161169
#Preview {
162170
@Previewable @State var text = ""
163-
@Previewable @State var height: CGFloat = 36
164171
@Previewable @FocusState var focused: Bool
165172
PasteInterceptingTextInput(
166173
text: $text,
167174
isFocused: $focused,
168175
isDisabled: false,
169-
placeholder: "Message...",
170-
dynamicHeight: $height
176+
placeholder: "Message..."
171177
)
172-
.frame(height: max(height, 36))
173178
.padding(.horizontal, 16)
174179
.padding(.vertical, 8)
175180
.glassEffect(in: .rect(cornerRadius: 20))

Wisp/Views/SpriteDetail/SpriteDetailView.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -237,7 +237,8 @@ struct SpriteDetailView: View {
237237
QuickActionsView(
238238
viewModel: vm,
239239
startChatCallback: { text in
240-
chatViewModel?.inputText += (chatViewModel?.inputText.isEmpty == true ? "" : "\n") + text
240+
guard let chatViewModel else { return }
241+
chatViewModel.inputText += (chatViewModel.inputText.isEmpty ? "" : "\n") + text
241242
selectedTab = .chat
242243
spriteQuickActionsViewModel = nil
243244
}

0 commit comments

Comments
 (0)