@@ -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,18 @@ 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 = if stack_id. is_some ( ) {
83+ "stack"
84+ } else {
85+ "project"
86+ } ;
8287 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."
88+ "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." ,
89+ mode
8490 ) ;
8591 } else {
8692 self . spawn_claude ( ctx. clone ( ) , broadcaster. clone ( ) , stack_id, user_params)
@@ -95,11 +101,17 @@ impl Claudes {
95101 & self ,
96102 ctx : Arc < Mutex < CommandContext > > ,
97103 broadcaster : Arc < tokio:: sync:: Mutex < Broadcaster > > ,
98- stack_id : StackId ,
104+ stack_id : Option < StackId > ,
99105 ) -> Result < ( ) > {
100106 if self . requests . lock ( ) . await . contains_key ( & stack_id) {
107+ let mode = if stack_id. is_some ( ) {
108+ "stack"
109+ } else {
110+ "project"
111+ } ;
101112 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."
113+ "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." ,
114+ mode
103115 )
104116 } else {
105117 self . compact ( ctx, broadcaster, stack_id) . await
@@ -111,7 +123,7 @@ impl Claudes {
111123 pub fn get_messages (
112124 & self ,
113125 ctx : & mut CommandContext ,
114- stack_id : StackId ,
126+ stack_id : Option < StackId > ,
115127 ) -> Result < Vec < ClaudeMessage > > {
116128 let rule = list_claude_assignment_rules ( ctx) ?
117129 . into_iter ( )
@@ -124,8 +136,8 @@ impl Claudes {
124136 }
125137 }
126138
127- /// Cancel a running Claude session for the given stack
128- pub async fn cancel_session ( & self , stack_id : StackId ) -> Result < bool > {
139+ /// Cancel a running Claude session for the given stack (or general session if None)
140+ pub async fn cancel_session ( & self , stack_id : Option < StackId > ) -> Result < bool > {
129141 let requests = self . requests . lock ( ) . await ;
130142 if let Some ( claude) = requests. get ( & stack_id) {
131143 // Send the kill signal
@@ -139,8 +151,8 @@ impl Claudes {
139151 }
140152 }
141153
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 {
154+ /// Check if there is an active Claude session for the given stack ID (or general session if None)
155+ pub async fn is_stack_active ( & self , stack_id : Option < StackId > ) -> bool {
144156 let requests = self . requests . lock ( ) . await ;
145157 requests. contains_key ( & stack_id)
146158 }
@@ -149,7 +161,7 @@ impl Claudes {
149161 & self ,
150162 ctx : Arc < Mutex < CommandContext > > ,
151163 broadcaster : Arc < tokio:: sync:: Mutex < Broadcaster > > ,
152- stack_id : StackId ,
164+ stack_id : Option < StackId > ,
153165 user_params : ClaudeUserParams ,
154166 ) -> ( ) {
155167 let res = self
@@ -182,7 +194,7 @@ impl Claudes {
182194 & self ,
183195 ctx : Arc < Mutex < CommandContext > > ,
184196 broadcaster : Arc < tokio:: sync:: Mutex < Broadcaster > > ,
185- stack_id : StackId ,
197+ stack_id : Option < StackId > ,
186198 user_params : ClaudeUserParams ,
187199 ) -> Result < ( ) > {
188200 // Capture the start time to filter messages created during this session
@@ -302,8 +314,12 @@ impl Claudes {
302314
303315 // Broadcast each new message
304316 for message in new_messages {
317+ let event_name = match stack_id {
318+ Some ( id) => format ! ( "project://{project_id}/claude/{id}/message_recieved" ) ,
319+ None => format ! ( "project://{project_id}/claude/general/message_recieved" ) ,
320+ } ;
305321 broadcaster. lock ( ) . await . send ( FrontendEvent {
306- name : format ! ( "project://{project_id}/claude/{stack_id}/message_recieved" ) ,
322+ name : event_name ,
307323 payload : serde_json:: json!( message) ,
308324 } ) ;
309325 }
@@ -325,7 +341,7 @@ impl Claudes {
325341async fn handle_exit (
326342 ctx : Arc < Mutex < CommandContext > > ,
327343 broadcaster : Arc < Mutex < Broadcaster > > ,
328- stack_id : StackId ,
344+ stack_id : Option < StackId > ,
329345 session_id : uuid:: Uuid ,
330346 mut read_stderr : PipeReader ,
331347 mut handle : Child ,
@@ -397,7 +413,7 @@ async fn spawn_command(
397413 ctx : Arc < Mutex < CommandContext > > ,
398414 user_params : ClaudeUserParams ,
399415 summary_to_resume : Option < String > ,
400- stack_id : StackId ,
416+ stack_id : Option < StackId > ,
401417) -> Result < Child > {
402418 // Write and obtain our own claude hooks path.
403419 let settings = fmt_claude_settings ( ) ?;
@@ -509,7 +525,7 @@ async fn spawn_command(
509525 // Format branch information for the system prompt
510526 let branch_info = {
511527 let mut ctx = ctx. lock ( ) . await ;
512- format_branch_info ( & mut ctx, stack_id)
528+ format_branch_info ( & mut ctx, stack_id) . await
513529 } ;
514530 let system_prompt = format ! ( "{}\n \n {}" , SYSTEM_PROMPT , branch_info) ;
515531 command. args ( [ "--append-system-prompt" , & system_prompt] ) ;
@@ -579,23 +595,43 @@ Sorry, this project is managed by GitButler so you must integrate upstream upstr
579595</git-usage>" ;
580596
581597/// 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 \
598+ async fn format_branch_info ( ctx : & mut CommandContext , stack_id : Option < StackId > ) -> String {
599+ let mut output = String :: from ( "<branch-info>\n " ) ;
600+
601+ output. push_str (
602+ "This repository uses GitButler for branch management. While git shows you are on\n \
586603 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 ",
604+ more independent stacks of branches being worked on simultaneously.\n \n ",
594605 ) ;
595606
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) ;
607+ match stack_id {
608+ Some ( stack_id) => {
609+ output. push_str (
610+ "This session is specific to a particular branch within that workspace. When asked about\n \
611+ the current branch or what changes have been made, you should focus on the current working\n \
612+ branch listed below, not the workspace branch itself.\n \n \
613+ Changes and diffs should be understood relative to the target branch (upstream), as that\n \
614+ represents the integration point for this work.\n \n \
615+ When asked about uncommitted changes you must only consider changes assigned to the stack.\n \n ",
616+ ) ;
617+
618+ append_target_branch_info ( & mut output, ctx) ;
619+ append_stack_branches_info ( & mut output, stack_id, ctx) ;
620+ append_assigned_files_info ( & mut output, stack_id, ctx) ;
621+ }
622+ None => {
623+ output. push_str (
624+ "This is a general project-wide session. You can see and work with all stacks and branches.\n \
625+ When the user asks about changes or branches without specifying which one, you should consider\n \
626+ all active stacks in the workspace.\n \n \
627+ You have access to all files and changes across all stacks.\n \n ",
628+ ) ;
629+
630+ append_target_branch_info ( & mut output, ctx) ;
631+ append_all_stacks_info ( & mut output, ctx) ;
632+ append_all_assigned_files_info ( & mut output, ctx) ;
633+ }
634+ }
599635
600636 output. push_str ( "</branch-info>" ) ;
601637 output
@@ -732,6 +768,119 @@ fn format_file_with_line_ranges(
732768 }
733769}
734770
771+ /// Appends information about all stacks in the workspace (for general sessions)
772+ fn append_all_stacks_info ( output : & mut String , ctx : & mut CommandContext ) {
773+ let repo = match ctx. gix_repo ( ) {
774+ Ok ( repo) => repo,
775+ Err ( e) => {
776+ tracing:: warn!( "Failed to get repository: {}" , e) ;
777+ output. push_str ( "Unable to fetch repository information.\n " ) ;
778+ return ;
779+ }
780+ } ;
781+
782+ match but_workspace:: legacy:: stacks (
783+ ctx,
784+ & ctx. project ( ) . gb_dir ( ) ,
785+ & repo,
786+ but_workspace:: legacy:: StacksFilter :: InWorkspace ,
787+ ) {
788+ Ok ( stacks) if !stacks. is_empty ( ) => {
789+ output. push_str ( "The following stacks are currently in the workspace:\n " ) ;
790+ for stack in stacks {
791+ if let Some ( stack_id) = stack. id {
792+ if let Some ( name) = stack. name ( ) {
793+ output. push_str ( & format ! (
794+ "- {} (stack_id: {})\n " ,
795+ name. to_str_lossy( ) ,
796+ stack_id
797+ ) ) ;
798+
799+ // List branches in this stack
800+ if !stack. heads . is_empty ( ) {
801+ output. push_str ( " Branches:\n " ) ;
802+ for head in & stack. heads {
803+ let checkout_marker = if head. is_checked_out {
804+ " (checked out)"
805+ } else {
806+ ""
807+ } ;
808+ output. push_str ( & format ! (
809+ " - {}{}\n " ,
810+ head. name. to_str_lossy( ) ,
811+ checkout_marker
812+ ) ) ;
813+ }
814+ }
815+ }
816+ }
817+ }
818+ }
819+ Ok ( _) => {
820+ output. push_str ( "There are no stacks currently in the workspace.\n " ) ;
821+ }
822+ Err ( e) => {
823+ tracing:: warn!( "Failed to fetch stacks: {}" , e) ;
824+ output. push_str ( "Unable to fetch stack information.\n " ) ;
825+ }
826+ }
827+ }
828+
829+ /// Appends information about all assigned files across all stacks (for general sessions)
830+ fn append_all_assigned_files_info ( output : & mut String , ctx : & mut CommandContext ) {
831+ let assignments = match but_hunk_assignment:: assignments_with_fallback (
832+ ctx,
833+ false ,
834+ None :: < Vec < but_core:: TreeChange > > ,
835+ None ,
836+ ) {
837+ Ok ( ( assignments, _error) ) => assignments,
838+ Err ( e) => {
839+ tracing:: warn!( "Failed to fetch hunk assignments: {}" , e) ;
840+ return ;
841+ }
842+ } ;
843+
844+ // Group assignments by stack_id
845+ let mut stacks_with_files: HashMap < Option < StackId > , Vec < & but_hunk_assignment:: HunkAssignment > > =
846+ HashMap :: new ( ) ;
847+ for assignment in & assignments {
848+ stacks_with_files
849+ . entry ( assignment. stack_id )
850+ . or_default ( )
851+ . push ( assignment) ;
852+ }
853+
854+ if stacks_with_files. is_empty ( ) {
855+ return ;
856+ }
857+
858+ output. push_str ( "\n File assignments across all stacks:\n " ) ;
859+
860+ // Show unassigned files first if any
861+ if let Some ( unassigned) = stacks_with_files. get ( & None ) {
862+ output. push_str ( "Unassigned files:\n " ) ;
863+ for assignment in unassigned {
864+ output. push_str ( & format ! ( " - {}\n " , assignment. path) ) ;
865+ }
866+ output. push ( '\n' ) ;
867+ }
868+
869+ // Show files grouped by stack
870+ let mut stack_ids: Vec < _ > = stacks_with_files. keys ( ) . filter_map ( |k| * k) . collect ( ) ;
871+ stack_ids. sort ( ) ;
872+
873+ for stack_id in stack_ids {
874+ if let Some ( files) = stacks_with_files. get ( & Some ( stack_id) ) {
875+ output. push_str ( & format ! ( "Stack {} files:\n " , stack_id) ) ;
876+ for assignment in files {
877+ output. push_str ( & format ! ( " - {}\n " , assignment. path) ) ;
878+ }
879+ output. push ( '\n' ) ;
880+ }
881+ }
882+ }
883+
735884fn format_message_with_summary (
736885 summary : & str ,
737886 message : & str ,
@@ -775,7 +924,7 @@ fn format_message(message: &str, thinking_level: ThinkingLevel) -> String {
775924async fn upsert_session (
776925 ctx : Arc < Mutex < CommandContext > > ,
777926 session_id : uuid:: Uuid ,
778- stack_id : StackId ,
927+ stack_id : Option < StackId > ,
779928) -> Result < crate :: ClaudeSession > {
780929 let mut ctx = ctx. lock ( ) . await ;
781930 let session = if let Some ( session) = db:: get_session_by_id ( & mut ctx, session_id) ? {
@@ -796,7 +945,7 @@ fn spawn_response_streaming(
796945 broadcaster : Arc < Mutex < Broadcaster > > ,
797946 read_stdout : PipeReader ,
798947 session_id : uuid:: Uuid ,
799- stack_id : StackId ,
948+ stack_id : Option < StackId > ,
800949) -> tokio:: task:: JoinHandle < ( ) > {
801950 tokio:: spawn ( async move {
802951 let ( tx, mut rx) = tokio:: sync:: mpsc:: unbounded_channel :: < String > ( ) ;
0 commit comments