Skip to content

Commit 4686fa5

Browse files
ochafikclaude
andcommitted
fix(swift-host): Robust teardown with single source of truth
- Move isTearingDown to ToolCallInfo (observable state) - Remove local @State that caused desync issues - Add 5-second timeout to prevent stuck cards - Guard against double-tap with early return - Card observes model state via @ObservedObject 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent b46c805 commit 4686fa5

File tree

2 files changed

+24
-10
lines changed

2 files changed

+24
-10
lines changed

examples/basic-host-swift/Sources/BasicHostApp/ContentView.swift

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -183,7 +183,6 @@ struct ToolCallCard: View {
183183
let onRemove: () -> Void
184184

185185
@State private var isInputExpanded = false
186-
@State private var isTearingDown = false
187186

188187
var body: some View {
189188
VStack(alignment: .leading, spacing: 8) {
@@ -200,7 +199,7 @@ struct ToolCallCard: View {
200199

201200
Spacer()
202201

203-
if isTearingDown {
202+
if toolCallInfo.isTearingDown {
204203
HStack(spacing: 4) {
205204
ProgressView().scaleEffect(0.6)
206205
Text("Closing...")
@@ -222,10 +221,7 @@ struct ToolCallCard: View {
222221
.foregroundColor(.secondary)
223222
}
224223

225-
Button {
226-
isTearingDown = true
227-
onRemove()
228-
} label: {
224+
Button(action: onRemove) {
229225
Image(systemName: "xmark")
230226
.font(.caption)
231227
.foregroundColor(.secondary)
@@ -290,8 +286,8 @@ struct ToolCallCard: View {
290286
.padding(10)
291287
.background(Color(UIColor.secondarySystemBackground))
292288
.cornerRadius(10)
293-
.opacity(isTearingDown ? 0.5 : 1.0)
294-
.animation(.easeInOut(duration: 0.2), value: isTearingDown)
289+
.opacity(toolCallInfo.isTearingDown ? 0.5 : 1.0)
290+
.animation(.easeInOut(duration: 0.2), value: toolCallInfo.isTearingDown)
295291
}
296292

297293
private var stateColor: Color {

examples/basic-host-swift/Sources/BasicHostApp/McpHostViewModel.swift

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,7 @@ class ToolCallInfo: ObservableObject, Identifiable {
288288
@Published var result: ToolResult?
289289
@Published var state: ExecutionState = .calling
290290
@Published var error: String?
291+
@Published var isTearingDown = false
291292

292293
init(
293294
serverName: String,
@@ -486,15 +487,32 @@ class ToolCallInfo: ObservableObject, Identifiable {
486487

487488
/// Teardown the app bridge before removing the tool call
488489
func teardown() async {
490+
// Prevent double-tap
491+
guard !isTearingDown else { return }
492+
isTearingDown = true
493+
489494
if let bridge = appBridge {
490495
do {
491-
_ = try await bridge.sendResourceTeardown()
496+
// Use a timeout so we don't wait forever if app is unresponsive
497+
try await withThrowingTaskGroup(of: Void.self) { group in
498+
group.addTask {
499+
_ = try await bridge.sendResourceTeardown()
500+
}
501+
group.addTask {
502+
try await Task.sleep(nanoseconds: 5_000_000_000) // 5 second timeout
503+
throw CancellationError()
504+
}
505+
// Wait for first to complete (either teardown or timeout)
506+
try await group.next()
507+
group.cancelAll()
508+
}
492509
} catch {
493-
print("[Host] Teardown failed: \(error)")
510+
print("[Host] Teardown failed or timed out: \(error)")
494511
}
495512
await bridge.close()
496513
}
497514
appBridge = nil
515+
// Note: isTearingDown stays true - the card will be removed from the list
498516
}
499517
}
500518

0 commit comments

Comments
 (0)