@@ -289,6 +289,7 @@ async fn status_line_workspace_changes_render_dirty_tracked_stats() {
289289 chat. status_line_workspace_changes = Some ( crate :: branch_summary:: GitWorkspaceDiffStats {
290290 additions : 830 ,
291291 deletions : 281 ,
292+ untracked_files : 0 ,
292293 } ) ;
293294
294295 assert_eq ! (
@@ -303,6 +304,7 @@ async fn status_line_workspace_changes_omits_clean_workspaces() {
303304 chat. status_line_workspace_changes = Some ( crate :: branch_summary:: GitWorkspaceDiffStats {
304305 additions : 0 ,
305306 deletions : 0 ,
307+ untracked_files : 0 ,
306308 } ) ;
307309
308310 assert_eq ! (
@@ -311,6 +313,69 @@ async fn status_line_workspace_changes_omits_clean_workspaces() {
311313 ) ;
312314}
313315
316+ #[ tokio:: test]
317+ async fn status_line_workspace_changes_render_untracked_file_count ( ) {
318+ let ( mut chat, _rx, _ops) = make_chatwidget_manual ( /*model_override*/ None ) . await ;
319+ chat. status_line_workspace_changes = Some ( crate :: branch_summary:: GitWorkspaceDiffStats {
320+ additions : 0 ,
321+ deletions : 0 ,
322+ untracked_files : 3 ,
323+ } ) ;
324+
325+ assert_eq ! (
326+ chat. status_line_value_for_item( crate :: bottom_pane:: StatusLineItem :: WorkspaceChanges ) ,
327+ Some ( "+0/-0 ?3" . to_string( ) )
328+ ) ;
329+ }
330+
331+ #[ tokio:: test]
332+ async fn status_line_workspace_changes_async_event_renders_untracked_stats ( ) {
333+ let ( mut chat, mut rx, _ops) = make_chatwidget_manual ( /*model_override*/ None ) . await ;
334+ chat. config . tui_status_line = Some ( vec ! [ "workspace-changes" . to_string( ) ] ) ;
335+ let expected_cwd = chat. config . cwd . to_path_buf ( ) ;
336+ install_scripted_workspace_command_runner (
337+ & mut chat,
338+ vec ! [
339+ workspace_response(
340+ & [ "git" , "rev-parse" , "--git-dir" ] ,
341+ /*exit_code*/ 0 ,
342+ ".git\n " ,
343+ ) ,
344+ workspace_response(
345+ & [ "git" , "diff" , "--numstat" , "HEAD" , "--" ] ,
346+ /*exit_code*/ 0 ,
347+ "2\t 1\t src/lib.rs\n " ,
348+ ) ,
349+ workspace_response(
350+ & [ "git" , "ls-files" , "--others" , "--exclude-standard" , "-z" ] ,
351+ /*exit_code*/ 0 ,
352+ "scratch.txt\0 " ,
353+ ) ,
354+ ] ,
355+ ) ;
356+ chat. status_line_workspace_changes_cwd = None ;
357+ chat. status_line_workspace_changes_lookup_complete = false ;
358+
359+ chat. refresh_status_line ( ) ;
360+ assert_eq ! ( status_line_text( & chat) , None ) ;
361+
362+ let event = tokio:: time:: timeout ( std:: time:: Duration :: from_secs ( 2 ) , rx. recv ( ) )
363+ . await
364+ . expect ( "workspace changes event" )
365+ . expect ( "workspace changes event" ) ;
366+ let crate :: app_event:: AppEvent :: StatusLineWorkspaceChangesUpdated { cwd, stats } = event else {
367+ panic ! ( "expected workspace changes event, got {event:?}" ) ;
368+ } ;
369+ assert_eq ! ( cwd, expected_cwd) ;
370+ chat. set_status_line_workspace_changes ( cwd, stats) ;
371+
372+ assert_eq ! (
373+ chat. status_line_workspace_changes_cwd. as_deref( ) ,
374+ Some ( expected_cwd. as_path( ) )
375+ ) ;
376+ assert_eq ! ( status_line_text( & chat) , Some ( "+2/-1 ?1" . to_string( ) ) ) ;
377+ }
378+
314379#[ tokio:: test]
315380async fn raw_output_status_line_value_only_shows_when_enabled ( ) {
316381 let ( mut chat, _rx, _ops) = make_chatwidget_manual ( /*model_override*/ None ) . await ;
@@ -1627,6 +1692,62 @@ fn install_noop_workspace_command_runner(chat: &mut ChatWidget) {
16271692 chat. workspace_command_runner = Some ( std:: sync:: Arc :: new ( NoopWorkspaceCommandRunner ) ) ;
16281693}
16291694
1695+ fn install_scripted_workspace_command_runner (
1696+ chat : & mut ChatWidget ,
1697+ responses : Vec < WorkspaceCommandResponse > ,
1698+ ) {
1699+ chat. workspace_command_runner = Some ( std:: sync:: Arc :: new ( ScriptedWorkspaceCommandRunner {
1700+ responses : std:: sync:: Mutex :: new ( responses. into ( ) ) ,
1701+ } ) ) ;
1702+ }
1703+
1704+ fn workspace_response ( argv : & [ & str ] , exit_code : i32 , stdout : & str ) -> WorkspaceCommandResponse {
1705+ WorkspaceCommandResponse {
1706+ argv : argv. iter ( ) . map ( |arg| ( * arg) . to_string ( ) ) . collect ( ) ,
1707+ output : crate :: workspace_command:: WorkspaceCommandOutput {
1708+ exit_code,
1709+ stdout : stdout. to_string ( ) ,
1710+ stderr : String :: new ( ) ,
1711+ } ,
1712+ }
1713+ }
1714+
1715+ struct WorkspaceCommandResponse {
1716+ argv : Vec < String > ,
1717+ output : crate :: workspace_command:: WorkspaceCommandOutput ,
1718+ }
1719+
1720+ struct ScriptedWorkspaceCommandRunner {
1721+ responses : std:: sync:: Mutex < std:: collections:: VecDeque < WorkspaceCommandResponse > > ,
1722+ }
1723+
1724+ impl crate :: workspace_command:: WorkspaceCommandExecutor for ScriptedWorkspaceCommandRunner {
1725+ fn run (
1726+ & self ,
1727+ command : crate :: workspace_command:: WorkspaceCommand ,
1728+ ) -> std:: pin:: Pin <
1729+ Box <
1730+ dyn std:: future:: Future <
1731+ Output = Result <
1732+ crate :: workspace_command:: WorkspaceCommandOutput ,
1733+ crate :: workspace_command:: WorkspaceCommandError ,
1734+ > ,
1735+ > + Send
1736+ + ' _ ,
1737+ > ,
1738+ > {
1739+ Box :: pin ( async move {
1740+ let mut responses = self . responses . lock ( ) . expect ( "responses lock" ) ;
1741+ let index = responses
1742+ . iter ( )
1743+ . position ( |response| response. argv == command. argv )
1744+ . unwrap_or_else ( || panic ! ( "missing fake response for {:?}" , command. argv) ) ;
1745+ let response = responses. remove ( index) . expect ( "fake response" ) ;
1746+ Ok ( response. output )
1747+ } )
1748+ }
1749+ }
1750+
16301751struct NoopWorkspaceCommandRunner ;
16311752
16321753impl crate :: workspace_command:: WorkspaceCommandExecutor for NoopWorkspaceCommandRunner {
0 commit comments