@@ -3,6 +3,7 @@ import ChatService
3
3
import Persist
4
4
import ComposableArchitecture
5
5
import GitHubCopilotService
6
+ import Combine
6
7
7
8
public let SELECTED_LLM_KEY = " selectedLLM "
8
9
@@ -28,6 +29,28 @@ extension AppState {
28
29
}
29
30
}
30
31
32
+ class CopilotModelManagerObservable : ObservableObject {
33
+ static let shared = CopilotModelManagerObservable ( )
34
+
35
+ @Published var availableChatModels : [ LLMModel ] = [ ]
36
+ @Published var defaultModel : LLMModel = . init( modelName: " " , modelFamily: " " )
37
+ private var cancellables = Set < AnyCancellable > ( )
38
+
39
+ private init ( ) {
40
+ // Initial load
41
+ availableChatModels = CopilotModelManager . getAvailableChatLLMs ( )
42
+
43
+ // Setup notification to update when models change
44
+ NotificationCenter . default. publisher ( for: . gitHubCopilotModelsDidChange)
45
+ . receive ( on: DispatchQueue . main)
46
+ . sink { [ weak self] _ in
47
+ self ? . availableChatModels = CopilotModelManager . getAvailableChatLLMs ( )
48
+ self ? . defaultModel = CopilotModelManager . getDefaultChatModel ( )
49
+ }
50
+ . store ( in: & cancellables)
51
+ }
52
+ }
53
+
31
54
extension CopilotModelManager {
32
55
static func getAvailableChatLLMs( ) -> [ LLMModel ] {
33
56
let LLMs = CopilotModelManager . getAvailableLLMs ( )
@@ -37,26 +60,40 @@ extension CopilotModelManager {
37
60
LLMModel ( modelName: $0. modelName, modelFamily: $0. modelFamily)
38
61
}
39
62
}
63
+
64
+ static func getDefaultChatModel( ) -> LLMModel {
65
+ let defaultModel = CopilotModelManager . getDefaultChatLLM ( )
66
+ if let defaultModel = defaultModel {
67
+ return LLMModel ( modelName: defaultModel. modelName, modelFamily: defaultModel. modelFamily)
68
+ }
69
+ // Fallback to a hardcoded default if no model has isChatDefault = true
70
+ return LLMModel ( modelName: " GPT-4.1 (Preview) " , modelFamily: " gpt-4.1 " )
71
+ }
40
72
}
41
73
42
74
struct LLMModel : Codable , Hashable {
43
75
let modelName : String
44
76
let modelFamily : String
45
77
}
46
78
47
- let defaultModel = LLMModel ( modelName: " GPT-4o " , modelFamily: " gpt-4o " )
48
79
struct ModelPicker : View {
49
- @State private var selectedModel = defaultModel . modelName
80
+ @State private var selectedModel = " "
50
81
@State private var isHovered = false
51
82
@State private var isPressed = false
83
+ @ObservedObject private var modelManager = CopilotModelManagerObservable . shared
52
84
static var lastRefreshModelsTime : Date = . init( timeIntervalSince1970: 0 )
53
85
54
86
init ( ) {
55
- self . updateCurrentModel ( )
87
+ let initialModel = AppState . shared. getSelectedModelName ( ) ?? CopilotModelManager . getDefaultChatModel ( ) . modelName
88
+ self . _selectedModel = State ( initialValue: initialModel)
56
89
}
57
90
58
91
var models : [ LLMModel ] {
59
- CopilotModelManager . getAvailableChatLLMs ( )
92
+ modelManager. availableChatModels
93
+ }
94
+
95
+ var defaultModel : LLMModel {
96
+ modelManager. defaultModel
60
97
}
61
98
62
99
func updateCurrentModel( ) {
@@ -65,44 +102,48 @@ struct ModelPicker: View {
65
102
66
103
var body : some View {
67
104
WithPerceptionTracking {
68
- Menu ( selectedModel) {
69
- if models. isEmpty {
70
- Button {
71
- // No action needed
72
- } label: {
73
- Text ( " Loading... " )
74
- }
75
- } else {
76
- ForEach ( models, id: \. self) { option in
77
- Button {
78
- selectedModel = option. modelName
79
- AppState . shared. setSelectedModel ( option)
80
- } label: {
81
- if selectedModel == option. modelName {
82
- Text ( " ✓ \( option. modelName) " )
83
- } else {
84
- Text ( " \( option. modelName) " )
105
+ Group {
106
+ if !models. isEmpty && !selectedModel. isEmpty {
107
+ Menu ( selectedModel) {
108
+ ForEach ( models, id: \. self) { option in
109
+ Button {
110
+ selectedModel = option. modelName
111
+ AppState . shared. setSelectedModel ( option)
112
+ } label: {
113
+ if selectedModel == option. modelName {
114
+ Text ( " ✓ \( option. modelName) " )
115
+ } else {
116
+ Text ( " \( option. modelName) " )
117
+ }
85
118
}
86
119
}
87
120
}
121
+ . menuStyle ( BorderlessButtonMenuStyle ( ) )
122
+ . frame ( maxWidth: labelWidth ( ) )
123
+ . padding ( 4 )
124
+ . background (
125
+ RoundedRectangle ( cornerRadius: 5 )
126
+ . fill ( isHovered ? Color . gray. opacity ( 0.1 ) : Color . clear)
127
+ )
128
+ . onHover { hovering in
129
+ isHovered = hovering
130
+ }
131
+ } else {
132
+ EmptyView ( )
88
133
}
89
134
}
90
- . menuStyle ( BorderlessButtonMenuStyle ( ) )
91
- . frame ( maxWidth: labelWidth ( ) )
92
- . padding ( 4 )
93
- . background (
94
- RoundedRectangle ( cornerRadius: 5 )
95
- . fill ( isHovered ? Color . gray. opacity ( 0.1 ) : Color . clear)
96
- )
97
- . onHover { hovering in
98
- isHovered = hovering
99
- }
100
135
. onAppear ( ) {
101
- updateCurrentModel ( )
102
136
Task {
103
137
await refreshModels ( )
138
+ updateCurrentModel ( )
104
139
}
105
140
}
141
+ . onChange ( of: defaultModel) { _ in
142
+ updateCurrentModel ( )
143
+ }
144
+ . onChange ( of: models) { _ in
145
+ updateCurrentModel ( )
146
+ }
106
147
. help ( " Pick Model " )
107
148
}
108
149
}
0 commit comments