ios: better backgrounding/resume
This commit is contained in:
@@ -96,9 +96,15 @@ final class SybilViewModel {
|
||||
var modelCatalog: [Provider: ProviderModelInfo] = [:]
|
||||
var model: String
|
||||
|
||||
@ObservationIgnored
|
||||
private var hasBootstrapped = false
|
||||
private var pendingChatState: PendingChatState?
|
||||
@ObservationIgnored
|
||||
private var selectionTask: Task<Void, Never>?
|
||||
@ObservationIgnored
|
||||
private var chatBackgroundTask: SybilBackgroundTaskAssertion?
|
||||
@ObservationIgnored
|
||||
private let clientFactory: (APIConfiguration) -> any SybilAPIClienting
|
||||
|
||||
private let fallbackModels: [Provider: [String]] = [
|
||||
.openai: ["gpt-4.1-mini"],
|
||||
@@ -106,8 +112,14 @@ final class SybilViewModel {
|
||||
.xai: ["grok-3-mini"]
|
||||
]
|
||||
|
||||
init(settings: SybilSettingsStore = SybilSettingsStore()) {
|
||||
init(
|
||||
settings: SybilSettingsStore = SybilSettingsStore(),
|
||||
clientFactory: @escaping (APIConfiguration) -> any SybilAPIClienting = { configuration in
|
||||
SybilAPIClient(configuration: configuration)
|
||||
}
|
||||
) {
|
||||
self.settings = settings
|
||||
self.clientFactory = clientFactory
|
||||
self.provider = settings.preferredProvider
|
||||
self.model = settings.preferredModelByProvider[settings.preferredProvider] ?? "gpt-4.1-mini"
|
||||
}
|
||||
@@ -281,6 +293,19 @@ final class SybilViewModel {
|
||||
return searches.first(where: { $0.id == searchID })
|
||||
}
|
||||
|
||||
var hasRefreshableSelection: Bool {
|
||||
guard draftKind == nil, let selectedItem else {
|
||||
return false
|
||||
}
|
||||
|
||||
switch selectedItem {
|
||||
case .chat, .search:
|
||||
return true
|
||||
case .settings:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func bootstrap() async {
|
||||
guard !hasBootstrapped else {
|
||||
return
|
||||
@@ -449,6 +474,30 @@ final class SybilViewModel {
|
||||
await reconnect()
|
||||
}
|
||||
|
||||
func refreshVisibleContent(refreshCollections shouldRefreshCollections: Bool, refreshSelection shouldRefreshSelection: Bool) async {
|
||||
guard isAuthenticated, !isCheckingSession else {
|
||||
return
|
||||
}
|
||||
|
||||
guard shouldRefreshCollections || shouldRefreshSelection else {
|
||||
return
|
||||
}
|
||||
|
||||
SybilLog.info(
|
||||
SybilLog.ui,
|
||||
"Foreground refresh requested (collections=\(shouldRefreshCollections), selection=\(shouldRefreshSelection))"
|
||||
)
|
||||
|
||||
if shouldRefreshCollections {
|
||||
await refreshCollections(preferredSelection: selectedItem, refreshSelection: shouldRefreshSelection)
|
||||
return
|
||||
}
|
||||
|
||||
if shouldRefreshSelection {
|
||||
await refreshSelectionIfNeeded()
|
||||
}
|
||||
}
|
||||
|
||||
func sendComposer() async {
|
||||
let content = composer.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
let attachments = composerAttachments
|
||||
@@ -540,7 +589,7 @@ final class SybilViewModel {
|
||||
|
||||
do {
|
||||
let client = try client()
|
||||
let chat = try await client.createChatFromSearch(searchID: search.id)
|
||||
let chat = try await client.createChatFromSearch(searchID: search.id, title: nil)
|
||||
draftKind = nil
|
||||
pendingChatState = nil
|
||||
composer = ""
|
||||
@@ -561,7 +610,7 @@ final class SybilViewModel {
|
||||
isCreatingSearchChat = false
|
||||
}
|
||||
|
||||
private func loadInitialData(using client: SybilAPIClient) async {
|
||||
private func loadInitialData(using client: any SybilAPIClienting) async {
|
||||
isLoadingCollections = true
|
||||
errorMessage = nil
|
||||
|
||||
@@ -633,7 +682,10 @@ final class SybilViewModel {
|
||||
settings.persist()
|
||||
}
|
||||
|
||||
private func refreshCollections(preferredSelection: SidebarSelection?) async {
|
||||
private func refreshCollections(
|
||||
preferredSelection: SidebarSelection?,
|
||||
refreshSelection: Bool = true
|
||||
) async {
|
||||
isLoadingCollections = true
|
||||
|
||||
do {
|
||||
@@ -665,7 +717,7 @@ final class SybilViewModel {
|
||||
selectedItem = sidebarItems.first?.selection
|
||||
}
|
||||
|
||||
if selectedItem != nil {
|
||||
if refreshSelection, selectedItem != nil {
|
||||
await refreshSelectionIfNeeded()
|
||||
}
|
||||
} catch {
|
||||
@@ -752,7 +804,7 @@ final class SybilViewModel {
|
||||
|
||||
var chatID = currentChatID
|
||||
if chatID == nil {
|
||||
let created = try await client.createChat()
|
||||
let created = try await client.createChat(title: nil)
|
||||
chatID = created.id
|
||||
draftKind = nil
|
||||
selectedItem = .chat(created.id)
|
||||
@@ -828,6 +880,15 @@ final class SybilViewModel {
|
||||
}
|
||||
}
|
||||
|
||||
chatBackgroundTask?.end()
|
||||
chatBackgroundTask = SybilBackgroundTaskAssertion(name: "Sybil Chat Response") {
|
||||
SybilLog.warning(SybilLog.app, "Chat response background time expired")
|
||||
}
|
||||
defer {
|
||||
chatBackgroundTask?.end()
|
||||
chatBackgroundTask = nil
|
||||
}
|
||||
|
||||
try await client.runCompletionStream(
|
||||
body: CompletionStreamRequest(
|
||||
chatId: chatID,
|
||||
@@ -1171,7 +1232,7 @@ final class SybilViewModel {
|
||||
return false
|
||||
}
|
||||
|
||||
private func client() throws -> SybilAPIClient {
|
||||
private func client() throws -> any SybilAPIClienting {
|
||||
guard let baseURL = settings.normalizedAPIBaseURL else {
|
||||
throw APIError.invalidBaseURL
|
||||
}
|
||||
@@ -1181,8 +1242,8 @@ final class SybilViewModel {
|
||||
"Creating API client for \(baseURL.absoluteString) (token: \(settings.trimmedTokenOrNil == nil ? "none" : "set"))"
|
||||
)
|
||||
|
||||
return SybilAPIClient(
|
||||
configuration: APIConfiguration(
|
||||
return clientFactory(
|
||||
APIConfiguration(
|
||||
baseURL: baseURL,
|
||||
authToken: settings.trimmedTokenOrNil
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user