@@ -514,16 +514,21 @@ export const chatsRouter = router({
514514 // Track workspace archived
515515 trackWorkspaceArchived ( input . id )
516516
517- // Kill terminal processes in background (don't await)
518- terminalManager . killByWorkspaceId ( input . id ) . then ( ( killResult ) => {
519- if ( killResult . killed > 0 ) {
520- console . log (
521- `[chats.archive] Killed ${ killResult . killed } terminal session(s) for workspace ${ input . id } ` ,
522- )
523- }
524- } ) . catch ( ( error ) => {
525- console . error ( `[chats.archive] Error killing processes:` , error )
526- } )
517+ // Kill terminal processes only for worktree-mode workspaces.
518+ // Local-mode terminals are shared across workspaces on the same project path,
519+ // so they should not be killed when a single workspace is archived.
520+ const isLocalMode = ! chat ?. branch
521+ if ( ! isLocalMode ) {
522+ terminalManager . killByWorkspaceId ( input . id ) . then ( ( killResult ) => {
523+ if ( killResult . killed > 0 ) {
524+ console . log (
525+ `[chats.archive] Killed ${ killResult . killed } terminal session(s) for workspace ${ input . id } ` ,
526+ )
527+ }
528+ } ) . catch ( ( error ) => {
529+ console . error ( `[chats.archive] Error killing processes:` , error )
530+ } )
531+ }
527532
528533 // Optionally delete worktree in background (don't await)
529534 if ( input . deleteWorktree && chat ?. worktreePath && chat ?. branch ) {
@@ -588,6 +593,14 @@ export const chatsRouter = router({
588593 const db = getDatabase ( )
589594 if ( input . chatIds . length === 0 ) return [ ]
590595
596+ // Identify worktree-mode workspaces before archiving (for terminal cleanup)
597+ const worktreeChats = db
598+ . select ( { id : chats . id , branch : chats . branch } )
599+ . from ( chats )
600+ . where ( inArray ( chats . id , input . chatIds ) )
601+ . all ( )
602+ . filter ( ( c ) => c . branch != null )
603+
591604 // Archive immediately (optimistic)
592605 const result = db
593606 . update ( chats )
@@ -596,19 +609,23 @@ export const chatsRouter = router({
596609 . returning ( )
597610 . all ( )
598611
599- // Kill terminal processes for all workspaces in background (don't await)
600- Promise . all (
601- input . chatIds . map ( ( id ) => terminalManager . killByWorkspaceId ( id ) ) ,
602- ) . then ( ( killResults ) => {
603- const totalKilled = killResults . reduce ( ( sum , r ) => sum + r . killed , 0 )
604- if ( totalKilled > 0 ) {
605- console . log (
606- `[chats.archiveBatch] Killed ${ totalKilled } terminal session(s) for ${ input . chatIds . length } workspace(s)` ,
607- )
608- }
609- } ) . catch ( ( error ) => {
610- console . error ( `[chats.archiveBatch] Error killing processes:` , error )
611- } )
612+ // Kill terminal processes only for worktree-mode workspaces.
613+ // Local-mode terminals are shared and should not be killed.
614+
615+ if ( worktreeChats . length > 0 ) {
616+ Promise . all (
617+ worktreeChats . map ( ( c ) => terminalManager . killByWorkspaceId ( c . id ) ) ,
618+ ) . then ( ( killResults ) => {
619+ const totalKilled = killResults . reduce ( ( sum , r ) => sum + r . killed , 0 )
620+ if ( totalKilled > 0 ) {
621+ console . log (
622+ `[chats.archiveBatch] Killed ${ totalKilled } terminal session(s) for ${ worktreeChats . length } worktree workspace(s)` ,
623+ )
624+ }
625+ } ) . catch ( ( error ) => {
626+ console . error ( `[chats.archiveBatch] Error killing processes:` , error )
627+ } )
628+ }
612629
613630 return result
614631 } ) ,
@@ -639,6 +656,14 @@ export const chatsRouter = router({
639656 }
640657 }
641658
659+ // Kill terminal processes for worktree-mode workspaces.
660+ // Local-mode terminals are shared and should not be killed on delete.
661+ if ( chat ?. branch ) {
662+ terminalManager . killByWorkspaceId ( input . id ) . catch ( ( error ) => {
663+ console . error ( `[chats.delete] Error killing processes:` , error )
664+ } )
665+ }
666+
642667 // Track workspace deleted
643668 trackWorkspaceDeleted ( input . id )
644669
@@ -710,6 +735,148 @@ export const chatsRouter = router({
710735 . get ( )
711736 } ) ,
712737
738+ /**
739+ * Fork a sub-chat from a specific message, preserving SDK session context.
740+ * Creates a new sub-chat with messages up to the target message,
741+ * copies the .jsonl session file, and marks it for forkSession resume.
742+ */
743+ forkSubChat : publicProcedure
744+ . input (
745+ z . object ( {
746+ subChatId : z . string ( ) ,
747+ messageId : z . string ( ) ,
748+ name : z . string ( ) . optional ( ) ,
749+ } ) ,
750+ )
751+ . mutation ( async ( { input } ) => {
752+ const db = getDatabase ( )
753+
754+ // 1. Get the source sub-chat
755+ const sourceSubChat = db
756+ . select ( )
757+ . from ( subChats )
758+ . where ( eq ( subChats . id , input . subChatId ) )
759+ . get ( )
760+ if ( ! sourceSubChat ) throw new Error ( "Source sub-chat not found" )
761+
762+ // 2. Parse messages and find the cutoff point
763+ const allMessages = JSON . parse ( sourceSubChat . messages || "[]" )
764+ const cutoffIndex = allMessages . findIndex (
765+ ( m : any ) => m . id === input . messageId ,
766+ )
767+ if ( cutoffIndex === - 1 ) throw new Error ( "Message not found" )
768+
769+ // 3. Slice messages up to and including the target
770+ const messagesToFork = allMessages . slice ( 0 , cutoffIndex + 1 )
771+
772+ // 4. Find sdkMessageUuid of last assistant message (for resumeSessionAt)
773+ const lastAssistant = [ ...messagesToFork ]
774+ . reverse ( )
775+ . find ( ( m : any ) => m . role === "assistant" )
776+ const forkAtSdkUuid = lastAssistant ?. metadata ?. sdkMessageUuid || null
777+
778+ // 5. Generate new IDs for all messages + set shouldForkResume on last assistant
779+ const forkedMessages = messagesToFork . map ( ( msg : any , i : number ) => ( {
780+ ...msg ,
781+ id : `fork-${ Date . now ( ) } -${ i } -${ Math . random ( ) . toString ( 36 ) . slice ( 2 , 7 ) } ` ,
782+ metadata : {
783+ ...msg . metadata ,
784+ shouldResume : undefined ,
785+ ...( msg === lastAssistant &&
786+ forkAtSdkUuid && {
787+ shouldForkResume : true ,
788+ } ) ,
789+ } ,
790+ } ) )
791+
792+ // 6. Generate fork name: [N] originalName
793+ let forkName = input . name
794+ if ( ! forkName ) {
795+ // Strip existing [N] prefix from source name to get base name
796+ const sourceName = sourceSubChat . name || "Chat"
797+ const baseName = sourceName . replace ( / ^ \[ \d + \] \s * / , "" )
798+
799+ // Find highest [N] among all sibling sub-chats
800+ const siblings = db
801+ . select ( { name : subChats . name } )
802+ . from ( subChats )
803+ . where ( eq ( subChats . chatId , sourceSubChat . chatId ) )
804+ . all ( )
805+
806+ let maxN = 0
807+ for ( const s of siblings ) {
808+ const match = s . name ?. match ( / ^ \[ ( \d + ) \] / )
809+ if ( match ) {
810+ maxN = Math . max ( maxN , parseInt ( match [ 1 ] , 10 ) )
811+ }
812+ }
813+
814+ forkName = `[${ maxN + 1 } ] ${ baseName } `
815+ }
816+
817+ // 7. Insert new sub-chat with sessionId from original (needed for resume)
818+ const newSubChat = db
819+ . insert ( subChats )
820+ . values ( {
821+ chatId : sourceSubChat . chatId ,
822+ name : forkName ,
823+ mode : sourceSubChat . mode ,
824+ messages : JSON . stringify ( forkedMessages ) ,
825+ sessionId : sourceSubChat . sessionId ,
826+ } )
827+ . returning ( )
828+ . get ( )
829+
830+ // 8. Copy .jsonl session files to the new isolated config dir
831+ if ( sourceSubChat . sessionId ) {
832+ try {
833+ const { app } = await import ( "electron" )
834+ const userDataPath = app . getPath ( "userData" )
835+ const sourceDir = path . join (
836+ userDataPath ,
837+ "claude-sessions" ,
838+ input . subChatId ,
839+ "projects" ,
840+ )
841+ const targetDir = path . join (
842+ userDataPath ,
843+ "claude-sessions" ,
844+ newSubChat . id ,
845+ "projects" ,
846+ )
847+
848+ const sourceDirExists = await fs
849+ . stat ( sourceDir )
850+ . then ( ( ) => true )
851+ . catch ( ( ) => false )
852+
853+ if ( sourceDirExists ) {
854+ await fs . cp ( sourceDir , targetDir , { recursive : true } )
855+ }
856+ } catch ( err ) {
857+ console . warn ( "[forkSubChat] Failed to copy session files:" , err )
858+ // Clear shouldForkResume since there's no .jsonl to fork from
859+ for ( const m of forkedMessages ) {
860+ if ( m . metadata ?. shouldForkResume ) {
861+ delete m . metadata . shouldForkResume
862+ }
863+ }
864+ db . update ( subChats )
865+ . set ( { messages : JSON . stringify ( forkedMessages ) } )
866+ . where ( eq ( subChats . id , newSubChat . id ) )
867+ . run ( )
868+ }
869+ }
870+
871+ console . log ( "[forkSubChat] Created" , { id : newSubChat . id , name : forkName , messages : forkedMessages . length } )
872+
873+ return {
874+ subChat : newSubChat ,
875+ messageCount : forkedMessages . length ,
876+ forkAtSdkUuid,
877+ }
878+ } ) ,
879+
713880 /**
714881 * Update sub-chat messages
715882 */
0 commit comments