import SwiftUI public struct SplitView: View { @State private var viewModel = SybilViewModel() @ObservedObject private var quickActionRouter = SybilQuickActionRouter.shared @Environment(\.horizontalSizeClass) private var horizontalSizeClass @Environment(\.scenePhase) private var scenePhase @State private var shouldRefreshOnForeground = false @State private var composerFocusRequest = 0 @State private var quickQuestionFocusRequest = 0 @State private var hasPendingQuickQuestionPresentation = false @State private var isQuickQuestionPresented = false @State private var columnVisibility: NavigationSplitViewVisibility = .automatic private var keyboardActions: SybilKeyboardActions? { guard !viewModel.isCheckingSession, viewModel.isAuthenticated else { return nil } return SybilKeyboardActions( newChat: { viewModel.startNewChat() composerFocusRequest += 1 }, newSearch: { viewModel.startNewSearch() composerFocusRequest += 1 }, previousConversation: { viewModel.selectPreviousSidebarItem() }, nextConversation: { viewModel.selectNextSidebarItem() } ) } @MainActor public init() { SybilFontRegistry.registerIfNeeded() SybilTheme.applySystemAppearance() } public var body: some View { ZStack { SybilTheme.backgroundGradient .ignoresSafeArea() if viewModel.isCheckingSession { ProgressView("Checking session…") .tint(SybilTheme.primary) .foregroundStyle(SybilTheme.textMuted) } else if !viewModel.isAuthenticated { SybilConnectionView(viewModel: viewModel) .padding() } else if horizontalSizeClass == .compact { SybilPhoneShellView(viewModel: viewModel) } else { GeometryReader { proxy in NavigationSplitView(columnVisibility: $columnVisibility) { SybilSidebarView(viewModel: viewModel) } detail: { SybilWorkspaceView( viewModel: viewModel, composerFocusRequest: composerFocusRequest, navigationLeadingControl: splitNavigationLeadingControl(for: proxy.size), onShowSidebar: showSidebar, onRequestNewChat: { viewModel.startNewChat() composerFocusRequest += 1 } ) } .navigationSplitViewStyle(.balanced) .tint(SybilTheme.primary) } } } .font(.sybil(.body)) .preferredColorScheme(.dark) .focusedSceneValue(\.sybilKeyboardActions, keyboardActions) .sheet(isPresented: $isQuickQuestionPresented, onDismiss: handleQuickQuestionDismissed) { SybilQuickQuestionView( viewModel: viewModel, focusRequest: quickQuestionFocusRequest ) .presentationDragIndicator(.visible) } .task { await viewModel.bootstrap() presentPendingQuickQuestionIfPossible() } .onReceive(quickActionRouter.$quickQuestionPresentationRequest) { request in guard request > 0 else { return } queueQuickQuestionPresentation() } .onChange(of: viewModel.isCheckingSession) { _, _ in presentPendingQuickQuestionIfPossible() } .onChange(of: viewModel.isAuthenticated) { _, _ in presentPendingQuickQuestionIfPossible() } .onChange(of: scenePhase) { _, nextPhase in switch nextPhase { case .background: shouldRefreshOnForeground = true viewModel.markAppInactiveForNetwork() case .active: viewModel.markAppActiveForNetwork() guard shouldRefreshOnForeground, horizontalSizeClass != .compact else { return } shouldRefreshOnForeground = false Task { await viewModel.refreshAfterAppBecameActive( refreshCollections: true, refreshSelection: viewModel.hasRefreshableSelection ) } case .inactive: shouldRefreshOnForeground = true viewModel.markAppInactiveForNetwork() @unknown default: break } } } private func splitNavigationLeadingControl(for size: CGSize) -> SybilWorkspaceNavigationLeadingControl { return size.width < size.height ? .showSidebar : .hidden } private func showSidebar() { withAnimation(.easeInOut(duration: 0.22)) { columnVisibility = .all } } private func queueQuickQuestionPresentation() { hasPendingQuickQuestionPresentation = true presentPendingQuickQuestionIfPossible() } private func presentPendingQuickQuestionIfPossible() { guard hasPendingQuickQuestionPresentation, !viewModel.isCheckingSession, viewModel.isAuthenticated else { return } hasPendingQuickQuestionPresentation = false quickQuestionFocusRequest += 1 isQuickQuestionPresented = true } private func handleQuickQuestionDismissed() { viewModel.cancelQuickQuestion() } } public struct SybilCommands: Commands { @FocusedValue(\.sybilKeyboardActions) private var keyboardActions public init() {} public var body: some Commands { CommandGroup(replacing: .newItem) { Button("New Chat") { keyboardActions?.newChat() } .keyboardShortcut("n", modifiers: .command) .disabled(keyboardActions == nil) Button("New Search") { keyboardActions?.newSearch() } .keyboardShortcut("n", modifiers: [.command, .shift]) .disabled(keyboardActions == nil) } CommandMenu("Conversation") { Button("Previous Conversation") { keyboardActions?.previousConversation() } .keyboardShortcut("[", modifiers: .command) .disabled(keyboardActions == nil) Button("Next Conversation") { keyboardActions?.nextConversation() } .keyboardShortcut("]", modifiers: .command) .disabled(keyboardActions == nil) } } } private struct SybilKeyboardActions { var newChat: () -> Void var newSearch: () -> Void var previousConversation: () -> Void var nextConversation: () -> Void } private struct SybilKeyboardActionsKey: FocusedValueKey { typealias Value = SybilKeyboardActions } private extension FocusedValues { var sybilKeyboardActions: SybilKeyboardActions? { get { self[SybilKeyboardActionsKey.self] } set { self[SybilKeyboardActionsKey.self] = newValue } } }