From 9572d0320f67205fa96186f8c53ff0e429439bca Mon Sep 17 00:00:00 2001 From: Sybil Codex Date: Sun, 3 May 2026 19:27:06 +0000 Subject: [PATCH] ios: add pull to refresh --- .../Sybil/SybilChatTranscriptView.swift | 5 ++ .../Sources/Sybil/SybilPhoneShellView.swift | 4 ++ .../Sybil/SybilSearchResultsView.swift | 5 ++ .../Sources/Sybil/SybilSidebarView.swift | 4 ++ .../Sybil/Sources/Sybil/SybilViewModel.swift | 52 +++++++++++++++++++ .../Sources/Sybil/SybilWorkspaceView.swift | 10 +++- 6 files changed, 78 insertions(+), 2 deletions(-) diff --git a/ios/Packages/Sybil/Sources/Sybil/SybilChatTranscriptView.swift b/ios/Packages/Sybil/Sources/Sybil/SybilChatTranscriptView.swift index a253eca..fed196e 100644 --- a/ios/Packages/Sybil/Sources/Sybil/SybilChatTranscriptView.swift +++ b/ios/Packages/Sybil/Sources/Sybil/SybilChatTranscriptView.swift @@ -5,6 +5,7 @@ struct SybilChatTranscriptView: View { var messages: [Message] var isLoading: Bool var isSending: Bool + var onRefresh: (() async -> Void)? = nil @State private var hasHandledInitialTranscriptScroll = false private var hasPendingAssistant: Bool { @@ -51,6 +52,10 @@ struct SybilChatTranscriptView: View { .padding(.vertical, 18) } .frame(maxWidth: .infinity, alignment: .leading) + .refreshable { + await onRefresh?() + } + .tint(SybilTheme.primary) .scrollDismissesKeyboard(.interactively) .onAppear { scrollToBottom(with: proxy, animated: false) diff --git a/ios/Packages/Sybil/Sources/Sybil/SybilPhoneShellView.swift b/ios/Packages/Sybil/Sources/Sybil/SybilPhoneShellView.swift index 7e5ae7f..e660891 100644 --- a/ios/Packages/Sybil/Sources/Sybil/SybilPhoneShellView.swift +++ b/ios/Packages/Sybil/Sources/Sybil/SybilPhoneShellView.swift @@ -132,6 +132,10 @@ private struct SybilPhoneSidebarRoot: View { } .padding(10) } + .refreshable { + await viewModel.refreshCollectionsFromUser() + } + .tint(SybilTheme.primary) } } .background(SybilTheme.panelGradient) diff --git a/ios/Packages/Sybil/Sources/Sybil/SybilSearchResultsView.swift b/ios/Packages/Sybil/Sources/Sybil/SybilSearchResultsView.swift index 04ca645..87224a2 100644 --- a/ios/Packages/Sybil/Sources/Sybil/SybilSearchResultsView.swift +++ b/ios/Packages/Sybil/Sources/Sybil/SybilSearchResultsView.swift @@ -6,6 +6,7 @@ struct SybilSearchResultsView: View { var isLoading: Bool var isRunning: Bool var isStartingChat: Bool = false + var onRefresh: (() async -> Void)? = nil var onStartChat: (() -> Void)? = nil var body: some View { @@ -100,6 +101,10 @@ struct SybilSearchResultsView: View { .padding(.horizontal, 14) .padding(.vertical, 20) } + .refreshable { + await onRefresh?() + } + .tint(SybilTheme.primary) .scrollDismissesKeyboard(.interactively) .frame(maxWidth: .infinity, alignment: .leading) } diff --git a/ios/Packages/Sybil/Sources/Sybil/SybilSidebarView.swift b/ios/Packages/Sybil/Sources/Sybil/SybilSidebarView.swift index 7719dbf..daae930 100644 --- a/ios/Packages/Sybil/Sources/Sybil/SybilSidebarView.swift +++ b/ios/Packages/Sybil/Sources/Sybil/SybilSidebarView.swift @@ -149,6 +149,10 @@ struct SybilSidebarView: View { } .padding(10) } + .refreshable { + await viewModel.refreshCollectionsFromUser() + } + .tint(SybilTheme.primary) } } diff --git a/ios/Packages/Sybil/Sources/Sybil/SybilViewModel.swift b/ios/Packages/Sybil/Sources/Sybil/SybilViewModel.swift index 7c1ed46..ef11b1d 100644 --- a/ios/Packages/Sybil/Sources/Sybil/SybilViewModel.swift +++ b/ios/Packages/Sybil/Sources/Sybil/SybilViewModel.swift @@ -471,6 +471,34 @@ final class SybilViewModel { select(nextSelection) } + func refreshCollectionsFromUser() async { + guard isAuthenticated else { + return + } + + errorMessage = nil + + guard draftKind == nil else { + await refreshCollectionsPreservingDraft() + return + } + + await refreshCollections(preferredSelection: selectedItem) + } + + func refreshSelectionFromUser() async { + guard isAuthenticated, !isSending, !isCreatingSearchChat else { + return + } + + guard selectedItem != nil, draftKind == nil else { + return + } + + errorMessage = nil + await refreshSelectionIfNeeded() + } + func deleteItem(_ selection: SidebarSelection) async { guard isAuthenticated else { return @@ -710,6 +738,30 @@ final class SybilViewModel { settings.persist() } + private func refreshCollectionsPreservingDraft() async { + isLoadingCollections = true + + do { + let client = try client() + async let chatsValue = client.listChats() + async let searchesValue = client.listSearches() + let (nextChats, nextSearches) = try await (chatsValue, searchesValue) + + chats = nextChats + searches = nextSearches + + SybilLog.info( + SybilLog.app, + "Refreshed collections for draft: \(nextChats.count) chats, \(nextSearches.count) searches" + ) + } catch { + errorMessage = normalizeAPIError(error) + SybilLog.error(SybilLog.app, "Refresh draft collections failed", error: error) + } + + isLoadingCollections = false + } + private func refreshCollections( preferredSelection: SidebarSelection?, refreshSelection: Bool = true diff --git a/ios/Packages/Sybil/Sources/Sybil/SybilWorkspaceView.swift b/ios/Packages/Sybil/Sources/Sybil/SybilWorkspaceView.swift index 6d27778..58edd76 100644 --- a/ios/Packages/Sybil/Sources/Sybil/SybilWorkspaceView.swift +++ b/ios/Packages/Sybil/Sources/Sybil/SybilWorkspaceView.swift @@ -127,7 +127,10 @@ struct SybilWorkspaceView: View { search: viewModel.selectedSearch, isLoading: viewModel.isLoadingSelection, isRunning: viewModel.isSending, - isStartingChat: viewModel.isCreatingSearchChat + isStartingChat: viewModel.isCreatingSearchChat, + onRefresh: { + await viewModel.refreshSelectionFromUser() + } ) { Task { await viewModel.startChatFromSelectedSearch() @@ -137,7 +140,10 @@ struct SybilWorkspaceView: View { SybilChatTranscriptView( messages: viewModel.displayedMessages, isLoading: viewModel.isLoadingSelection, - isSending: viewModel.isSending + isSending: viewModel.isSending, + onRefresh: { + await viewModel.refreshSelectionFromUser() + } ) .id(transcriptScrollContextID) }