@@ -37,10 +37,24 @@ struct SettingsView: View {
3737 return ModelCatalog . all. first ( where: { $0. id == modelId } ) ? . isMoE ?? false
3838 }
3939
40+ private var currentModelId : String ? {
41+ guard case . ready( let modelId) = engine. state else { return nil }
42+ return modelId
43+ }
44+
4045 private var effectiveStreamExpertsSetting : Bool {
4146 viewModel. config. effectiveStreamExperts ( defaultingTo: currentModelIsMoE)
4247 }
4348
49+ // Tracks the stream-experts value that was in effect when the current model was loaded.
50+ // A mismatch with `effectiveStreamExpertsSetting` means a reload is required.
51+ @State private var appliedStreamExperts : Bool ? = nil
52+
53+ private var needsModelReloadForStreamingChange : Bool {
54+ guard let applied = appliedStreamExperts else { return false }
55+ return effectiveStreamExpertsSetting != applied
56+ }
57+
4458 private var ssdStreamingBinding : Binding < Bool > {
4559 Binding (
4660 get: { effectiveStreamExpertsSetting } ,
@@ -117,6 +131,16 @@ struct SettingsView: View {
117131 }
118132 . onAppear {
119133 draftServerConfiguration = server. startupConfiguration
134+ // Seed the applied value from the current engine state so the reload
135+ // prompt doesn't fire spuriously on first open.
136+ if case . ready = engine. state {
137+ appliedStreamExperts = effectiveStreamExpertsSetting
138+ }
139+ }
140+ . onChange ( of: engine. state) { _, newState in
141+ if case . ready = newState {
142+ appliedStreamExperts = effectiveStreamExpertsSetting
143+ }
120144 }
121145 #if os(macOS)
122146 . frame( minWidth: 520 , minHeight: 580 )
@@ -295,6 +319,9 @@ struct SettingsView: View {
295319 tint: SwiftBuddyTheme . warning,
296320 hint: " Stream MoE expert weights from NVMe (requires model reload) "
297321 )
322+ if needsModelReloadForStreamingChange {
323+ modelReloadPrompt
324+ }
298325 toggleRow (
299326 label: " TurboQuant KV " , icon: " bolt.badge.clock " ,
300327 isOn: $viewModel. config. turboKV,
@@ -555,70 +582,8 @@ struct SettingsView: View {
555582 tint: SwiftBuddyTheme . accentSecondary,
556583 hint: " mmap expert weights from NVMe — only active expert pages stay in RAM. Auto-enabled for MoE catalog models. "
557584 )
558- if effectiveStreamExpertsSetting != currentModelIsMoE {
559- VStack ( alignment: . leading, spacing: 8 ) {
560- HStack ( spacing: 6 ) {
561- Image ( systemName: " arrow.clockwise.circle.fill " )
562- . foregroundStyle ( SwiftBuddyTheme . warning)
563- . font ( . caption)
564- Text ( " Reload model to apply this change " )
565- . font ( . caption2. weight ( . medium) )
566- . foregroundStyle ( SwiftBuddyTheme . warning)
567- Spacer ( )
568- Button ( " Reload " ) {
569- let currentId : String ? = {
570- if case . ready( let id) = engine. state { return id }
571- return nil
572- } ( )
573- if let id = currentId {
574- Task {
575- engine. unload ( )
576- await engine. load ( modelId: id)
577- }
578- }
579- }
580- . font ( . caption2. weight ( . semibold) )
581- . foregroundStyle ( SwiftBuddyTheme . accent)
582- . buttonStyle ( . plain)
583- }
584-
585- switch engine. state {
586- case . loading( let progress, let stage) :
587- VStack ( alignment: . leading, spacing: 4 ) {
588- HStack {
589- Text ( stage)
590- . font ( . caption2. weight ( . medium) )
591- . foregroundStyle ( SwiftBuddyTheme . textSecondary)
592- Spacer ( )
593- Text ( " \( Int ( progress * 100 ) ) % " )
594- . font ( . caption2. monospacedDigit ( ) )
595- . foregroundStyle ( SwiftBuddyTheme . textTertiary)
596- }
597- ProgressView ( value: progress)
598- . tint ( SwiftBuddyTheme . accent)
599- }
600- case . downloading( let progress, let speed) :
601- VStack ( alignment: . leading, spacing: 4 ) {
602- HStack {
603- Text ( " Downloading model files " )
604- . font ( . caption2. weight ( . medium) )
605- . foregroundStyle ( SwiftBuddyTheme . textSecondary)
606- Spacer ( )
607- Text ( " \( Int ( progress * 100 ) ) % · \( speed) " )
608- . font ( . caption2. monospacedDigit ( ) )
609- . foregroundStyle ( SwiftBuddyTheme . textTertiary)
610- }
611- ProgressView ( value: progress)
612- . tint ( SwiftBuddyTheme . accent)
613- }
614- default :
615- EmptyView ( )
616- }
617- }
618- . padding ( . horizontal, 4 )
619- . padding ( . vertical, 6 )
620- . background ( SwiftBuddyTheme . warning. opacity ( 0.08 ) )
621- . clipShape ( RoundedRectangle ( cornerRadius: 8 ) )
585+ if needsModelReloadForStreamingChange {
586+ modelReloadPrompt
622587 }
623588 }
624589 }
@@ -981,6 +946,63 @@ struct SettingsView: View {
981946 }
982947 }
983948
949+ @ViewBuilder
950+ private var modelReloadPrompt : some View {
951+ VStack ( alignment: . leading, spacing: 8 ) {
952+ HStack ( spacing: 6 ) {
953+ Image ( systemName: " arrow.clockwise.circle.fill " )
954+ . foregroundStyle ( SwiftBuddyTheme . warning)
955+ . font ( . caption)
956+ Text ( " Reload model to apply this change " )
957+ . font ( . caption2. weight ( . medium) )
958+ . foregroundStyle ( SwiftBuddyTheme . warning)
959+ Spacer ( )
960+ Button ( " Reload " ) {
961+ reloadCurrentModel ( )
962+ }
963+ . font ( . caption2. weight ( . semibold) )
964+ . foregroundStyle ( SwiftBuddyTheme . accent)
965+ . buttonStyle ( . plain)
966+ . disabled ( currentModelId == nil )
967+ }
968+
969+ switch engine. state {
970+ case . loading( let progress, let stage) :
971+ progressRow ( label: stage, progress: progress)
972+ case . downloading( let progress, let speed) :
973+ progressRow ( label: " Downloading · \( speed) " , progress: progress)
974+ default :
975+ EmptyView ( )
976+ }
977+ }
978+ }
979+
980+ @ViewBuilder
981+ private func progressRow( label: String , progress: Double ) -> some View {
982+ VStack ( alignment: . leading, spacing: 4 ) {
983+ HStack {
984+ Text ( label)
985+ . font ( . caption2. weight ( . medium) )
986+ . foregroundStyle ( SwiftBuddyTheme . textSecondary)
987+ Spacer ( )
988+ Text ( " \( Int ( progress * 100 ) ) % " )
989+ . font ( . caption2. monospacedDigit ( ) )
990+ . foregroundStyle ( SwiftBuddyTheme . textTertiary)
991+ }
992+ ProgressView ( value: progress)
993+ . tint ( SwiftBuddyTheme . accent)
994+ . controlSize ( . small)
995+ }
996+ }
997+
998+ private func reloadCurrentModel( ) {
999+ guard let currentModelId else { return }
1000+ Task {
1001+ engine. unload ( )
1002+ await engine. load ( modelId: currentModelId)
1003+ }
1004+ }
1005+
9841006 @ViewBuilder
9851007 private func parameterCard< Content: View > ( _ title: String , @ViewBuilder content: ( ) -> Content ) -> some View {
9861008 VStack ( alignment: . leading, spacing: 10 ) {
0 commit comments