@@ -100,11 +100,190 @@ public async Task RefreshIgnoresCancellationDuringWorkspaceProbe()
100100 ( await model . FeedbackMessage ) . Should ( ) . BeEmpty ( ) ;
101101 }
102102
103+ [ Test ]
104+ public async Task FleetBoardShowsTheActiveSessionWhileStreamingAndClearsAfterCompletion ( )
105+ {
106+ await using var fixture = await CreateFixtureAsync ( ) ;
107+ ( await fixture . WorkspaceState . CreateAgentAsync (
108+ new CreateAgentProfileCommand (
109+ "Fleet Agent" ,
110+ AgentProviderKind . Debug ,
111+ "debug-echo" ,
112+ "Stay deterministic for fleet board verification." ) ,
113+ CancellationToken . None ) ) . ShouldSucceed ( ) ;
114+ var model = ActivatorUtilities . CreateInstance < ChatModel > ( fixture . Provider ) ;
115+
116+ await model . StartNewSession ( CancellationToken . None ) ;
117+ var selectedChat = await model . SelectedChat ;
118+
119+ await using var enumerator = fixture . WorkspaceState . SendMessageAsync (
120+ new SendSessionMessageCommand ( selectedChat ! . Id , "fleet activity" ) ,
121+ CancellationToken . None )
122+ . GetAsyncEnumerator ( CancellationToken . None ) ;
123+
124+ var observedLiveBoard = false ;
125+ while ( await enumerator . MoveNextAsync ( ) )
126+ {
127+ _ = enumerator . Current . ShouldSucceed ( ) ;
128+ var board = await model . FleetBoard ;
129+ board . Should ( ) . NotBeNull ( ) ;
130+ if ( board ! . ActiveSessions . Count == 0 )
131+ {
132+ continue ;
133+ }
134+
135+ observedLiveBoard = true ;
136+ board . Metrics . Should ( ) . Contain ( metric =>
137+ metric . Label == "Live sessions" &&
138+ metric . Value == "1" ) ;
139+ board . ActiveSessions . Should ( ) . Contain ( item =>
140+ item . Title == selectedChat . Title &&
141+ item . Summary . Contains ( "Fleet Agent" , StringComparison . Ordinal ) ) ;
142+ break ;
143+ }
144+
145+ observedLiveBoard . Should ( ) . BeTrue ( ) ;
146+
147+ while ( await enumerator . MoveNextAsync ( ) )
148+ {
149+ _ = enumerator . Current . ShouldSucceed ( ) ;
150+ }
151+
152+ FleetBoardView ? completedBoard = null ;
153+ var timeoutAt = DateTimeOffset . UtcNow . AddSeconds ( 2 ) ;
154+ while ( DateTimeOffset . UtcNow < timeoutAt )
155+ {
156+ completedBoard = await model . FleetBoard ;
157+ completedBoard . Should ( ) . NotBeNull ( ) ;
158+ if ( completedBoard ! . ActiveSessions . Count == 0 )
159+ {
160+ break ;
161+ }
162+
163+ await Task . Delay ( 50 ) ;
164+ }
165+
166+ completedBoard . Should ( ) . NotBeNull ( ) ;
167+ completedBoard ! . ActiveSessions . Should ( ) . BeEmpty ( ) ;
168+ completedBoard . ShowActiveSessionsEmptyState . Should ( ) . BeTrue ( ) ;
169+ }
170+
171+ [ Test ]
172+ public async Task FleetBoardReusesTheWarmProviderSnapshotDuringLiveStreaming ( )
173+ {
174+ using var commandScope = CodexCliTestScope . Create ( nameof ( ChatModelTests ) ) ;
175+ commandScope . WriteCountingVersionCommand ( "codex" , "codex version 1.0.0" , delayMilliseconds : 300 ) ;
176+ commandScope . WriteCodexMetadata ( "gpt-5.4" , "gpt-5.4" ) ;
177+ await using var fixture = await CreateFixtureAsync ( ) ;
178+ ( await fixture . WorkspaceState . CreateAgentAsync (
179+ new CreateAgentProfileCommand (
180+ "Fleet Agent" ,
181+ AgentProviderKind . Debug ,
182+ "debug-echo" ,
183+ "Stay deterministic for fleet board verification." ) ,
184+ CancellationToken . None ) ) . ShouldSucceed ( ) ;
185+ var model = ActivatorUtilities . CreateInstance < ChatModel > ( fixture . Provider ) ;
186+
187+ var initialBoard = await model . FleetBoard ;
188+ initialBoard . Should ( ) . NotBeNull ( ) ;
189+ var warmInvocationCount = commandScope . ReadInvocationCount ( "codex" ) ;
190+
191+ await model . StartNewSession ( CancellationToken . None ) ;
192+ var postStartInvocationCount = commandScope . ReadInvocationCount ( "codex" ) ;
193+ postStartInvocationCount . Should ( ) . BeGreaterThanOrEqualTo ( warmInvocationCount ) ;
194+
195+ var selectedChat = await model . SelectedChat ;
196+
197+ await using var enumerator = fixture . WorkspaceState . SendMessageAsync (
198+ new SendSessionMessageCommand ( selectedChat ! . Id , "fleet activity" ) ,
199+ CancellationToken . None )
200+ . GetAsyncEnumerator ( CancellationToken . None ) ;
201+
202+ var observedLiveBoard = false ;
203+ while ( await enumerator . MoveNextAsync ( ) )
204+ {
205+ _ = enumerator . Current . ShouldSucceed ( ) ;
206+ var board = await model . FleetBoard ;
207+ board . Should ( ) . NotBeNull ( ) ;
208+ if ( board ! . ActiveSessions . Count == 0 )
209+ {
210+ continue ;
211+ }
212+
213+ observedLiveBoard = true ;
214+ break ;
215+ }
216+
217+ observedLiveBoard . Should ( ) . BeTrue ( ) ;
218+ commandScope . ReadInvocationCount ( "codex" ) . Should ( ) . Be ( postStartInvocationCount ) ;
219+
220+ while ( await enumerator . MoveNextAsync ( ) )
221+ {
222+ _ = enumerator . Current . ShouldSucceed ( ) ;
223+ }
224+ }
225+
226+ [ Test ]
227+ public async Task OpenFleetSessionSelectsTheRequestedActiveSession ( )
228+ {
229+ await using var fixture = await CreateFixtureAsync ( ) ;
230+ var agent = ( await fixture . WorkspaceState . CreateAgentAsync (
231+ new CreateAgentProfileCommand (
232+ "Navigator Agent" ,
233+ AgentProviderKind . Debug ,
234+ "debug-echo" ,
235+ "Stay deterministic for fleet navigation verification." ) ,
236+ CancellationToken . None ) ) . ShouldSucceed ( ) ;
237+ var firstSession = ( await fixture . WorkspaceState . CreateSessionAsync (
238+ new CreateSessionCommand ( "Fleet Session One" , agent . Id ) ,
239+ CancellationToken . None ) ) . ShouldSucceed ( ) ;
240+ var secondSession = ( await fixture . WorkspaceState . CreateSessionAsync (
241+ new CreateSessionCommand ( "Fleet Session Two" , agent . Id ) ,
242+ CancellationToken . None ) ) . ShouldSucceed ( ) ;
243+ var model = ActivatorUtilities . CreateInstance < ChatModel > ( fixture . Provider ) ;
244+ await model . SelectedChat . UpdateAsync (
245+ _ => new SessionSidebarItem ( secondSession . Session . Id , secondSession . Session . Title , secondSession . Session . Preview ) ,
246+ CancellationToken . None ) ;
247+
248+ await using var enumerator = fixture . WorkspaceState . SendMessageAsync (
249+ new SendSessionMessageCommand ( firstSession . Session . Id , "jump back to this" ) ,
250+ CancellationToken . None )
251+ . GetAsyncEnumerator ( CancellationToken . None ) ;
252+
253+ FleetBoardSessionItem ? activeSession = null ;
254+ while ( await enumerator . MoveNextAsync ( ) )
255+ {
256+ _ = enumerator . Current . ShouldSucceed ( ) ;
257+ var board = await model . FleetBoard ;
258+ board . Should ( ) . NotBeNull ( ) ;
259+ activeSession = board ! . ActiveSessions . FirstOrDefault ( item =>
260+ item . Title == firstSession . Session . Title ) ;
261+ if ( activeSession is not null )
262+ {
263+ break ;
264+ }
265+ }
266+
267+ activeSession . Should ( ) . NotBeNull ( ) ;
268+ await model . OpenFleetSession ( activeSession ! . OpenRequest , CancellationToken . None ) ;
269+
270+ var selectedChat = await model . SelectedChat ;
271+ selectedChat . Should ( ) . NotBeNull ( ) ;
272+ selectedChat ! . Id . Should ( ) . Be ( firstSession . Session . Id ) ;
273+ selectedChat . Title . Should ( ) . Be ( firstSession . Session . Title ) ;
274+
275+ while ( await enumerator . MoveNextAsync ( ) )
276+ {
277+ _ = enumerator . Current . ShouldSucceed ( ) ;
278+ }
279+ }
280+
103281 private static async Task < TestFixture > CreateFixtureAsync ( )
104282 {
105283 var services = new ServiceCollection ( ) ;
106284 services . AddSingleton ( TimeProvider . System ) ;
107285 services . AddSingleton < WorkspaceProjectionNotifier > ( ) ;
286+ services . AddSingleton < UiDispatcher > ( ) ;
108287 services . AddSingleton < IOperatorPreferencesStore , LocalOperatorPreferencesStore > ( ) ;
109288 services . AddAgentSessions ( new AgentSessionStorageOptions
110289 {
0 commit comments