@@ -3,79 +3,103 @@ import SwiftUI
33struct ChatStatusBar : View {
44 let status : ChatStatus
55 let modelName : String ?
6+ @Binding var modelOverride : ClaudeModel ?
67 var hasPendingWispAsk : Bool = false
78
9+ @AppStorage ( " claudeModel " ) private var globalModel : String = ClaudeModel . sonnet. rawValue
10+
11+ private var effectiveModel : ClaudeModel {
12+ modelOverride ?? ClaudeModel ( rawValue: globalModel) ?? . sonnet
13+ }
14+
815 private var statusKey : String {
916 switch status {
10- case . idle: return " idle- \( modelName ?? " " ) "
17+ case . idle: return " idle- \( modelName ?? " " ) - \( effectiveModel . rawValue ) "
1118 case . connecting: return " connecting "
1219 case . streaming: return hasPendingWispAsk ? " waiting " : " streaming "
1320 case . reconnecting: return " reconnecting "
1421 case . error( let message) : return " error- \( message) "
1522 }
1623 }
1724
18- private var isVisible : Bool {
19- switch status {
20- case . idle : return modelName != nil
21- case . connecting , . streaming , . reconnecting , . error : return true
25+ var body : some View {
26+ HStack {
27+ statusPill
28+ Spacer ( )
2229 }
30+ . padding ( . horizontal)
2331 }
2432
25- var body : some View {
26- if isVisible {
27- HStack {
28- statusPill
29- Spacer ( )
33+ private var statusPill : some View {
34+ HStack ( spacing: 6 ) {
35+ switch status {
36+ case . idle:
37+ modelPicker
38+ case . connecting:
39+ ProgressView ( )
40+ . controlSize ( . mini)
41+ Text ( " Connecting... " )
42+ . font ( . caption2)
43+ . foregroundStyle ( . secondary)
44+ case . streaming:
45+ ProgressView ( )
46+ . controlSize ( . mini)
47+ Text ( hasPendingWispAsk ? " Waiting for answer... " : " Streaming... " )
48+ . font ( . caption2)
49+ . foregroundStyle ( . secondary)
50+ case . reconnecting:
51+ ProgressView ( )
52+ . controlSize ( . mini)
53+ Text ( " Reconnecting... " )
54+ . font ( . caption2)
55+ . foregroundStyle ( . orange)
56+ case . error( let message) :
57+ Image ( systemName: " exclamationmark.triangle.fill " )
58+ . foregroundStyle ( . red)
59+ . font ( . system( size: 10 ) )
60+ Text ( message)
61+ . font ( . caption2)
62+ . foregroundStyle ( . red)
63+ . lineLimit ( 1 )
3064 }
31- . padding ( . horizontal)
3265 }
66+ . padding ( . horizontal, 10 )
67+ . padding ( . vertical, 4 )
68+ . glassEffect ( )
69+ . animation ( . easeInOut( duration: 0.2 ) , value: statusKey)
3370 }
3471
35- private var statusPill : some View {
36- HStack ( spacing: 6 ) {
37- switch status {
38- case . idle:
39- if let modelName {
40- Image ( systemName: " checkmark.circle.fill " )
41- . foregroundStyle ( . green)
42- . font ( . system( size: 10 ) )
43- Text ( modelName)
44- . font ( . caption2)
45- . foregroundStyle ( . secondary)
72+ private var modelPicker : some View {
73+ Menu {
74+ ForEach ( ClaudeModel . allCases) { model in
75+ Button {
76+ if model. rawValue == globalModel {
77+ modelOverride = nil
78+ } else {
79+ modelOverride = model
80+ }
81+ } label: {
82+ HStack {
83+ Text ( model. displayName)
84+ if model == effectiveModel {
85+ Image ( systemName: " checkmark " )
86+ }
4687 }
47- case . connecting:
48- ProgressView ( )
49- . controlSize ( . mini)
50- Text ( " Connecting... " )
51- . font ( . caption2)
52- . foregroundStyle ( . secondary)
53- case . streaming:
54- ProgressView ( )
55- . controlSize ( . mini)
56- Text ( hasPendingWispAsk ? " Waiting for answer... " : " Streaming... " )
57- . font ( . caption2)
58- . foregroundStyle ( . secondary)
59- case . reconnecting:
60- ProgressView ( )
61- . controlSize ( . mini)
62- Text ( " Reconnecting... " )
63- . font ( . caption2)
64- . foregroundStyle ( . orange)
65- case . error( let message) :
66- Image ( systemName: " exclamationmark.triangle.fill " )
67- . foregroundStyle ( . red)
68- . font ( . system( size: 10 ) )
69- Text ( message)
70- . font ( . caption2)
71- . foregroundStyle ( . red)
72- . lineLimit ( 1 )
7388 }
7489 }
75- . padding ( . horizontal, 10 )
76- . padding ( . vertical, 4 )
77- . glassEffect ( )
78- . animation ( . easeInOut( duration: 0.2 ) , value: statusKey)
90+ } label: {
91+ HStack ( spacing: 4 ) {
92+ Image ( systemName: " sparkle " )
93+ . foregroundStyle ( . secondary)
94+ . font ( . system( size: 9 ) )
95+ Text ( effectiveModel. displayName)
96+ . font ( . caption2)
97+ . foregroundStyle ( . secondary)
98+ Image ( systemName: " chevron.up.chevron.down " )
99+ . foregroundStyle ( . tertiary)
100+ . font ( . system( size: 8 ) )
101+ }
102+ }
79103 }
80104}
81105
@@ -85,38 +109,44 @@ private let previewBackground = LinearGradient(
85109 endPoint: . bottomTrailing
86110)
87111
88- #Preview( " Idle " ) {
89- ChatStatusBar ( status: . idle, modelName: " claude-sonnet-4-5-20250929 " )
112+ #Preview( " Idle - Model Picker " ) {
113+ @Previewable @State var modelOverride : ClaudeModel ? = nil
114+ ChatStatusBar ( status: . idle, modelName: " claude-sonnet-4-5-20250929 " , modelOverride: $modelOverride)
90115 . frame ( maxWidth: . infinity, maxHeight: . infinity)
91116 . background ( previewBackground)
92117}
93118
94119#Preview( " Streaming " ) {
95- ChatStatusBar ( status: . streaming, modelName: nil )
120+ @Previewable @State var modelOverride : ClaudeModel ? = nil
121+ ChatStatusBar ( status: . streaming, modelName: nil , modelOverride: $modelOverride)
96122 . frame ( maxWidth: . infinity, maxHeight: . infinity)
97123 . background ( previewBackground)
98124}
99125
100126#Preview( " Connecting " ) {
101- ChatStatusBar ( status: . connecting, modelName: nil )
127+ @Previewable @State var modelOverride : ClaudeModel ? = nil
128+ ChatStatusBar ( status: . connecting, modelName: nil , modelOverride: $modelOverride)
102129 . frame ( maxWidth: . infinity, maxHeight: . infinity)
103130 . background ( previewBackground)
104131}
105132
106133#Preview( " Reconnecting " ) {
107- ChatStatusBar ( status: . reconnecting, modelName: nil )
134+ @Previewable @State var modelOverride : ClaudeModel ? = nil
135+ ChatStatusBar ( status: . reconnecting, modelName: nil , modelOverride: $modelOverride)
108136 . frame ( maxWidth: . infinity, maxHeight: . infinity)
109137 . background ( previewBackground)
110138}
111139
112140#Preview( " Error " ) {
113- ChatStatusBar ( status: . error( " Connection lost " ) , modelName: nil )
141+ @Previewable @State var modelOverride : ClaudeModel ? = nil
142+ ChatStatusBar ( status: . error( " Connection lost " ) , modelName: nil , modelOverride: $modelOverride)
114143 . frame ( maxWidth: . infinity, maxHeight: . infinity)
115144 . background ( previewBackground)
116145}
117146
118147#Preview( " All States " ) {
119148 @Previewable @State var stateIndex = 0
149+ @Previewable @State var modelOverride : ClaudeModel ? = nil
120150
121151 let states : [ ( ChatStatus , String ? ) ] = [
122152 ( . connecting, nil ) ,
@@ -129,7 +159,8 @@ private let previewBackground = LinearGradient(
129159 VStack ( spacing: 20 ) {
130160 ChatStatusBar (
131161 status: states [ stateIndex] . 0 ,
132- modelName: states [ stateIndex] . 1
162+ modelName: states [ stateIndex] . 1 ,
163+ modelOverride: $modelOverride
133164 )
134165
135166 Button ( " Next State " ) {
0 commit comments