@@ -53,11 +53,11 @@ use crate::{
5353 send_claude_message,
5454} ;
5555
56- /// Holds the CC instances. Currently keyed by stackId, since our current model
57- /// assumes one CC per stack at any given time .
56+ /// Holds the CC instances. Keyed by optional stackId - None represents a general
57+ /// project-wide session, Some(StackId) represents a stack-specific session .
5858pub struct Claudes {
5959 /// A set that contains all the currently running requests
60- pub ( crate ) requests : Mutex < HashMap < StackId , Arc < Claude > > > ,
60+ pub ( crate ) requests : Mutex < HashMap < Option < StackId > , Arc < Claude > > > ,
6161}
6262
6363pub struct Claude {
@@ -75,12 +75,14 @@ impl Claudes {
7575 & self ,
7676 ctx : Arc < Mutex < CommandContext > > ,
7777 broadcaster : Arc < tokio:: sync:: Mutex < Broadcaster > > ,
78- stack_id : StackId ,
78+ stack_id : Option < StackId > ,
7979 user_params : ClaudeUserParams ,
8080 ) -> Result < ( ) > {
8181 if self . requests . lock ( ) . await . contains_key ( & stack_id) {
82+ let mode = stack_id. map_or ( "project" , |_| "stack" ) ;
8283 bail ! (
83- "Claude is currently thinking, please wait for it to complete before sending another message.\n \n If claude is stuck thinking, try restarting the application."
84+ "Claude is currently thinking for this {}, please wait for it to complete before sending another message.\n \n If claude is stuck thinking, try restarting the application." ,
85+ mode
8486 ) ;
8587 } else {
8688 self . spawn_claude ( ctx. clone ( ) , broadcaster. clone ( ) , stack_id, user_params)
@@ -95,11 +97,13 @@ impl Claudes {
9597 & self ,
9698 ctx : Arc < Mutex < CommandContext > > ,
9799 broadcaster : Arc < tokio:: sync:: Mutex < Broadcaster > > ,
98- stack_id : StackId ,
100+ stack_id : Option < StackId > ,
99101 ) -> Result < ( ) > {
100102 if self . requests . lock ( ) . await . contains_key ( & stack_id) {
103+ let mode = stack_id. map_or ( "project" , |_| "stack" ) ;
101104 bail ! (
102- "Claude is currently thinking, please wait for it to complete before sending another message.\n \n If claude is stuck thinking, try restarting the application."
105+ "Claude is currently thinking for this {}, please wait for it to complete before sending another message.\n \n If claude is stuck thinking, try restarting the application." ,
106+ mode
103107 )
104108 } else {
105109 self . compact ( ctx, broadcaster, stack_id) . await
@@ -111,7 +115,7 @@ impl Claudes {
111115 pub fn get_messages (
112116 & self ,
113117 ctx : & mut CommandContext ,
114- stack_id : StackId ,
118+ stack_id : Option < StackId > ,
115119 ) -> Result < Vec < ClaudeMessage > > {
116120 let rule = list_claude_assignment_rules ( ctx) ?
117121 . into_iter ( )
@@ -124,8 +128,8 @@ impl Claudes {
124128 }
125129 }
126130
127- /// Cancel a running Claude session for the given stack
128- pub async fn cancel_session ( & self , stack_id : StackId ) -> Result < bool > {
131+ /// Cancel a running Claude session for the given stack (or general session if None)
132+ pub async fn cancel_session ( & self , stack_id : Option < StackId > ) -> Result < bool > {
129133 let requests = self . requests . lock ( ) . await ;
130134 if let Some ( claude) = requests. get ( & stack_id) {
131135 // Send the kill signal
@@ -139,8 +143,8 @@ impl Claudes {
139143 }
140144 }
141145
142- /// Check if there is an active Claude session for the given stack ID
143- pub async fn is_stack_active ( & self , stack_id : StackId ) -> bool {
146+ /// Check if there is an active Claude session for the given stack ID (or general session if None)
147+ pub async fn is_stack_active ( & self , stack_id : Option < StackId > ) -> bool {
144148 let requests = self . requests . lock ( ) . await ;
145149 requests. contains_key ( & stack_id)
146150 }
@@ -149,7 +153,7 @@ impl Claudes {
149153 & self ,
150154 ctx : Arc < Mutex < CommandContext > > ,
151155 broadcaster : Arc < tokio:: sync:: Mutex < Broadcaster > > ,
152- stack_id : StackId ,
156+ stack_id : Option < StackId > ,
153157 user_params : ClaudeUserParams ,
154158 ) -> ( ) {
155159 let res = self
@@ -182,7 +186,7 @@ impl Claudes {
182186 & self ,
183187 ctx : Arc < Mutex < CommandContext > > ,
184188 broadcaster : Arc < tokio:: sync:: Mutex < Broadcaster > > ,
185- stack_id : StackId ,
189+ stack_id : Option < StackId > ,
186190 user_params : ClaudeUserParams ,
187191 ) -> Result < ( ) > {
188192 // Capture the start time to filter messages created during this session
@@ -303,7 +307,7 @@ impl Claudes {
303307 // Broadcast each new message
304308 for message in new_messages {
305309 broadcaster. lock ( ) . await . send ( FrontendEvent {
306- name : format ! ( "project://{ project_id}/claude/{ stack_id}/message_recieved" ) ,
310+ name : crate :: claude_event_name ( project_id, stack_id) ,
307311 payload : serde_json:: json!( message) ,
308312 } ) ;
309313 }
@@ -325,7 +329,7 @@ impl Claudes {
325329async fn handle_exit (
326330 ctx : Arc < Mutex < CommandContext > > ,
327331 broadcaster : Arc < Mutex < Broadcaster > > ,
328- stack_id : StackId ,
332+ stack_id : Option < StackId > ,
329333 session_id : uuid:: Uuid ,
330334 mut read_stderr : PipeReader ,
331335 mut handle : Child ,
@@ -397,7 +401,7 @@ async fn spawn_command(
397401 ctx : Arc < Mutex < CommandContext > > ,
398402 user_params : ClaudeUserParams ,
399403 summary_to_resume : Option < String > ,
400- stack_id : StackId ,
404+ stack_id : Option < StackId > ,
401405) -> Result < Child > {
402406 // Write and obtain our own claude hooks path.
403407 let settings = fmt_claude_settings ( ) ?;
@@ -509,7 +513,7 @@ async fn spawn_command(
509513 // Format branch information for the system prompt
510514 let branch_info = {
511515 let mut ctx = ctx. lock ( ) . await ;
512- format_branch_info ( & mut ctx, stack_id)
516+ format_branch_info ( & mut ctx, stack_id) . await
513517 } ;
514518 let system_prompt = format ! ( "{}\n \n {}" , SYSTEM_PROMPT , branch_info) ;
515519 command. args ( [ "--append-system-prompt" , & system_prompt] ) ;
@@ -579,23 +583,43 @@ Sorry, this project is managed by GitButler so you must integrate upstream upstr
579583</git-usage>" ;
580584
581585/// Formats branch information for the system prompt
582- fn format_branch_info ( ctx : & mut CommandContext , stack_id : StackId ) -> String {
583- let mut output = String :: from (
584- "<branch-info>\n \
585- This repository uses GitButler for branch management. While git shows you are on\n \
586+ async fn format_branch_info ( ctx : & mut CommandContext , stack_id : Option < StackId > ) -> String {
587+ let mut output = String :: from ( "<branch-info>\n " ) ;
588+
589+ output. push_str (
590+ "This repository uses GitButler for branch management. While git shows you are on\n \
586591 the `gitbutler/workspace` branch, this is actually a merge commit containing one or\n \
587- more independent stacks of branches being worked on simultaneously.\n \n \
588- This session is specific to a particular branch within that workspace. When asked about\n \
589- the current branch or what changes have been made, you should focus on the current working\n \
590- branch listed below, not the workspace branch itself.\n \n \
591- Changes and diffs should be understood relative to the target branch (upstream), as that\n \
592- represents the integration point for this work.\n \n \
593- When asked about uncommitted changes you must only consider changes assigned to the stack.\n \n ",
592+ more independent stacks of branches being worked on simultaneously.\n \n ",
594593 ) ;
595594
596- append_target_branch_info ( & mut output, ctx) ;
597- append_stack_branches_info ( & mut output, stack_id, ctx) ;
598- append_assigned_files_info ( & mut output, stack_id, ctx) ;
595+ match stack_id {
596+ Some ( stack_id) => {
597+ output. push_str (
598+ "This session is specific to a particular branch within that workspace. When asked about\n \
599+ the current branch or what changes have been made, you should focus on the current working\n \
600+ branch listed below, not the workspace branch itself.\n \n \
601+ Changes and diffs should be understood relative to the target branch (upstream), as that\n \
602+ represents the integration point for this work.\n \n \
603+ When asked about uncommitted changes you must only consider changes assigned to the stack.\n \n ",
604+ ) ;
605+
606+ append_target_branch_info ( & mut output, ctx) ;
607+ append_stack_branches_info ( & mut output, stack_id, ctx) ;
608+ append_assigned_files_info ( & mut output, stack_id, ctx) ;
609+ }
610+ None => {
611+ output. push_str (
612+ "This is a general project-wide session. You can see and work with all stacks and branches.\n \
613+ When the user asks about changes or branches without specifying which one, you should consider\n \
614+ all active stacks in the workspace.\n \n \
615+ You have access to all files and changes across all stacks.\n \n ",
616+ ) ;
617+
618+ append_target_branch_info ( & mut output, ctx) ;
619+ append_all_stacks_info ( & mut output, ctx) ;
620+ append_all_assigned_files_info ( & mut output, ctx) ;
621+ }
622+ }
599623
600624 output. push_str ( "</branch-info>" ) ;
601625 output
@@ -732,6 +756,103 @@ fn format_file_with_line_ranges(
732756 }
733757}
734758
759+ /// Appends information about all stacks in the workspace (for general sessions)
760+ fn append_all_stacks_info ( output : & mut String , ctx : & mut CommandContext ) {
761+ let Ok ( repo) = ctx. gix_repo ( ) else {
762+ tracing:: warn!( "Failed to get repository" ) ;
763+ output. push_str ( "Unable to fetch repository information.\n " ) ;
764+ return ;
765+ } ;
766+
767+ let stacks = match but_workspace:: legacy:: stacks (
768+ ctx,
769+ & ctx. project ( ) . gb_dir ( ) ,
770+ & repo,
771+ but_workspace:: legacy:: StacksFilter :: InWorkspace ,
772+ ) {
773+ Ok ( stacks) if !stacks. is_empty ( ) => stacks,
774+ Ok ( _) => {
775+ output. push_str ( "There are no stacks currently in the workspace.\n " ) ;
776+ return ;
777+ }
778+ Err ( e) => {
779+ tracing:: warn!( "Failed to fetch stacks: {e}" ) ;
780+ output. push_str ( "Unable to fetch stack information.\n " ) ;
781+ return ;
782+ }
783+ } ;
784+
785+ output. push_str ( "The following stacks are currently in the workspace:\n " ) ;
786+ for stack in stacks {
787+ let ( Some ( stack_id) , Some ( name) ) = ( stack. id , stack. name ( ) ) else {
788+ continue ;
789+ } ;
790+
791+ output. push_str ( & format ! ( "- {} (stack_id: {stack_id})\n " , name. to_str_lossy( ) ) ) ;
792+
793+ // List branches in this stack
794+ if !stack. heads . is_empty ( ) {
795+ output. push_str ( " Branches:\n " ) ;
796+ for head in & stack. heads {
797+ let checkout_marker = if head. is_checked_out { " (checked out)" } else { "" } ;
798+ output. push_str ( & format ! ( " - {}{checkout_marker}\n " , head. name. to_str_lossy( ) ) ) ;
799+ }
800+ }
801+ }
802+ }
803+
804+ /// Appends information about all assigned files across all stacks (for general sessions)
805+ fn append_all_assigned_files_info ( output : & mut String , ctx : & mut CommandContext ) {
806+ let Ok ( ( assignments, _error) ) = but_hunk_assignment:: assignments_with_fallback (
807+ ctx,
808+ false ,
809+ None :: < Vec < but_core:: TreeChange > > ,
810+ None ,
811+ ) else {
812+ tracing:: warn!( "Failed to fetch hunk assignments" ) ;
813+ return ;
814+ } ;
815+
816+ // Group assignments by stack_id
817+ let mut stacks_with_files: HashMap < Option < StackId > , Vec < & but_hunk_assignment:: HunkAssignment > > =
818+ HashMap :: new ( ) ;
819+ for assignment in & assignments {
820+ stacks_with_files
821+ . entry ( assignment. stack_id )
822+ . or_default ( )
823+ . push ( assignment) ;
824+ }
825+
826+ if stacks_with_files. is_empty ( ) {
827+ return ;
828+ }
829+
830+ output. push_str ( "\n File assignments across all stacks:\n " ) ;
831+
832+ // Show unassigned files first if any
833+ if let Some ( unassigned) = stacks_with_files. get ( & None ) {
834+ output. push_str ( "Unassigned files:\n " ) ;
835+ for assignment in unassigned {
836+ output. push_str ( & format ! ( " - {}\n " , assignment. path) ) ;
837+ }
838+ output. push ( '\n' ) ;
839+ }
840+
841+ // Show files grouped by stack
842+ let mut stack_ids: Vec < _ > = stacks_with_files. keys ( ) . copied ( ) . flatten ( ) . collect ( ) ;
843+ stack_ids. sort ( ) ;
844+
845+ for stack_id in stack_ids {
846+ if let Some ( files) = stacks_with_files. get ( & Some ( stack_id) ) {
847+ output. push_str ( & format ! ( "Stack {stack_id} files:\n " ) ) ;
848+ for assignment in files {
849+ output. push_str ( & format ! ( " - {}\n " , assignment. path) ) ;
850+ }
851+ output. push ( '\n' ) ;
852+ }
853+ }
854+ }
855+
735856fn format_message_with_summary (
736857 summary : & str ,
737858 message : & str ,
@@ -775,7 +896,7 @@ fn format_message(message: &str, thinking_level: ThinkingLevel) -> String {
775896async fn upsert_session (
776897 ctx : Arc < Mutex < CommandContext > > ,
777898 session_id : uuid:: Uuid ,
778- stack_id : StackId ,
899+ stack_id : Option < StackId > ,
779900) -> Result < crate :: ClaudeSession > {
780901 let mut ctx = ctx. lock ( ) . await ;
781902 let session = if let Some ( session) = db:: get_session_by_id ( & mut ctx, session_id) ? {
@@ -796,7 +917,7 @@ fn spawn_response_streaming(
796917 broadcaster : Arc < Mutex < Broadcaster > > ,
797918 read_stdout : PipeReader ,
798919 session_id : uuid:: Uuid ,
799- stack_id : StackId ,
920+ stack_id : Option < StackId > ,
800921) -> tokio:: task:: JoinHandle < ( ) > {
801922 tokio:: spawn ( async move {
802923 let ( tx, mut rx) = tokio:: sync:: mpsc:: unbounded_channel :: < String > ( ) ;
0 commit comments