Skip to content

Commit b822e81

Browse files
authored
Merge pull request #81 from mcintyre94/chat-with-claude-about-command-outputs-e3bc1cba
Add "Start Chat" button to Bash quick actions outside of chat context
2 parents 87a41c3 + 54b0ae2 commit b822e81

File tree

5 files changed

+101
-33
lines changed

5 files changed

+101
-33
lines changed

Wisp/Views/QuickActions/BashQuickView.swift

Lines changed: 37 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ struct BashQuickView: View {
44
@Environment(SpritesAPIClient.self) private var apiClient
55
@Bindable var viewModel: BashQuickViewModel
66
var onInsert: ((String) -> Void)? = nil
7+
var onStartChat: ((String) -> Void)? = nil
78
@FocusState private var isInputFocused: Bool
89

910
private let shellChars = ["/", "-", "|", ">", "~", "`", "$", "&", "*", "."]
@@ -109,12 +110,20 @@ struct BashQuickView: View {
109110
.padding(.vertical, 8)
110111
.padding(.bottom, isRunningOnMac ? 12 : 0)
111112

112-
if let onInsert, !viewModel.output.isEmpty, !viewModel.isRunning {
113-
Button("Insert into chat") {
114-
onInsert(viewModel.insertFormatted())
113+
if !viewModel.output.isEmpty, !viewModel.isRunning {
114+
if let onInsert {
115+
Button("Insert into chat") {
116+
onInsert(viewModel.insertFormatted())
117+
}
118+
.buttonStyle(.borderedProminent)
119+
.padding(.bottom, 8)
120+
} else if let onStartChat {
121+
Button("Start Chat") {
122+
onStartChat(viewModel.insertFormatted())
123+
}
124+
.buttonStyle(.borderedProminent)
125+
.padding(.bottom, 8)
115126
}
116-
.buttonStyle(.borderedProminent)
117-
.padding(.bottom, 8)
118127
}
119128
}
120129
.background(Color(.systemBackground))
@@ -124,7 +133,7 @@ struct BashQuickView: View {
124133
}
125134
}
126135

127-
#Preview {
136+
#Preview("No callback") {
128137
BashQuickView(
129138
viewModel: BashQuickViewModel(
130139
spriteName: "my-sprite",
@@ -133,3 +142,25 @@ struct BashQuickView: View {
133142
)
134143
.environment(SpritesAPIClient())
135144
}
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: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ struct QuickActionsView: View {
55
@Environment(\.dismiss) private var dismiss
66
let viewModel: QuickActionsViewModel
77
var insertCallback: ((String) -> Void)? = nil
8+
var startChatCallback: ((String) -> Void)? = nil
89

910
@State private var selectedTab = 0
1011

@@ -43,10 +44,15 @@ struct QuickActionsView: View {
4344

4445
@ViewBuilder private var bashTab: some View {
4546
if let cb = insertCallback {
46-
BashQuickView(viewModel: viewModel.bashViewModel) { text in
47+
BashQuickView(viewModel: viewModel.bashViewModel, onInsert: { text in
4748
cb(text)
4849
dismiss()
49-
}
50+
})
51+
} else if let cb = startChatCallback {
52+
BashQuickView(viewModel: viewModel.bashViewModel, onStartChat: { text in
53+
cb(text)
54+
dismiss()
55+
})
5056
} else {
5157
BashQuickView(viewModel: viewModel.bashViewModel)
5258
}
@@ -59,7 +65,7 @@ struct QuickActionsView: View {
5965
}
6066
}
6167

62-
#Preview {
68+
#Preview("No callback") {
6369
QuickActionsView(
6470
viewModel: QuickActionsViewModel(
6571
spriteName: "my-sprite",
@@ -69,3 +75,27 @@ struct QuickActionsView: View {
6975
)
7076
.environment(SpritesAPIClient())
7177
}
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: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -234,10 +234,18 @@ struct SpriteDetailView: View {
234234
ChatSwitcherSheet(viewModel: chatListViewModel)
235235
}
236236
.sheet(item: $spriteQuickActionsViewModel) { vm in
237-
QuickActionsView(viewModel: vm)
238-
.environment(apiClient)
239-
.presentationDetents([.medium, .large])
240-
.presentationDragIndicator(.visible)
237+
QuickActionsView(
238+
viewModel: vm,
239+
startChatCallback: { text in
240+
guard let chatViewModel else { return }
241+
chatViewModel.inputText += (chatViewModel.inputText.isEmpty ? "" : "\n") + text
242+
selectedTab = .chat
243+
spriteQuickActionsViewModel = nil
244+
}
245+
)
246+
.environment(apiClient)
247+
.presentationDetents([.medium, .large])
248+
.presentationDragIndicator(.visible)
241249
}
242250
.alert("Sprite Recreated", isPresented: $showStaleChatsAlert) {
243251
Button("Start Fresh", role: .destructive) {

0 commit comments

Comments
 (0)