@@ -107,6 +107,7 @@ import {
107107 pendingBuildPlanSubChatIdAtom ,
108108 pendingPlanApprovalsAtom ,
109109 planEditRefetchTriggerAtomFamily ,
110+ workspaceDiffCacheAtomFamily ,
110111 pendingPrMessageAtom ,
111112 pendingReviewMessageAtom ,
112113 pendingUserQuestionsAtom ,
@@ -2806,6 +2807,14 @@ const ChatViewInner = memo(function ChatViewInner({
28062807 // Update store mode synchronously BEFORE sending (transport reads from store)
28072808 useAgentSubChatStore . getState ( ) . updateSubChatMode ( subChatId , "agent" )
28082809
2810+ // Sync mode to database for sidebar indicator (getPendingPlanApprovals)
2811+ if ( ! subChatId . startsWith ( "temp-" ) ) {
2812+ updateSubChatModeMutation . mutate ( { subChatId, mode : "agent" } )
2813+ }
2814+
2815+ // Update ref BEFORE setIsPlanMode to prevent useEffect from triggering duplicate mutation
2816+ lastIsPlanModeRef . current = false
2817+
28092818 // Update React state (for UI)
28102819 setIsPlanMode ( false )
28112820
@@ -2818,7 +2827,7 @@ const ChatViewInner = memo(function ChatViewInner({
28182827 role : "user" ,
28192828 parts : [ { type : "text" , text : "Build plan" } ] ,
28202829 } )
2821- } , [ subChatId , setIsPlanMode , scrollToBottom ] )
2830+ } , [ subChatId , setIsPlanMode , scrollToBottom , updateSubChatModeMutation ] )
28222831
28232832 // Handle pending "Build plan" from sidebar
28242833 useEffect ( ( ) => {
@@ -3342,9 +3351,11 @@ const ChatViewInner = memo(function ChatViewInner({
33423351 return `@[${ MENTION_PREFIXES . DIFF } ${ dtc . filePath } :${ lineNum } :${ preview } :${ encodedText } ]`
33433352 } )
33443353
3345- // Add pasted text files as file mentions (they are already saved as files)
3354+ // Add pasted text as pasted mentions (format: pasted:size:preview|filepath)
3355+ // Using | as separator since filepath can contain colons
33463356 const pastedTextMentions = currentPastedTexts . map ( ( pt ) => {
3347- return `@[${ MENTION_PREFIXES . FILE } local:${ pt . filePath } ]`
3357+ // Preview is already truncated and has newlines replaced
3358+ return `@[${ MENTION_PREFIXES . PASTED } ${ pt . size } :${ pt . preview } |${ pt . filePath } ]`
33483359 } )
33493360
33503361 mentionPrefix = [ ...quoteMentions , ...diffMentions , ...pastedTextMentions ] . join ( " " ) + " "
@@ -3632,21 +3643,15 @@ const ChatViewInner = memo(function ChatViewInner({
36323643 }
36333644 }
36343645
3635- // Check if there's an unapproved plan (ExitPlanMode without subsequent "Build plan" or "Implement plan" )
3646+ // Check if there's an unapproved plan (in plan mode with completed ExitPlanMode )
36363647 const hasUnapprovedPlan = useMemo ( ( ) => {
3637- // Traverse messages from end to find unapproved ExitPlanMode
3648+ // If already in agent mode, plan is approved (mode is the source of truth)
3649+ if ( ! isPlanMode ) return false
3650+
3651+ // Look for completed ExitPlanMode in messages
36383652 for ( let i = messages . length - 1 ; i >= 0 ; i -- ) {
36393653 const msg = messages [ i ]
36403654
3641- // If user message says "Build plan" or "Implement plan", plan is already approved
3642- if ( msg . role === "user" ) {
3643- const text = msg . parts ?. find ( ( p : any ) => p . type === "text" ) ?. text || ""
3644- const normalizedText = text . trim ( ) . toLowerCase ( )
3645- if ( normalizedText === "build plan" || normalizedText === "implement plan" ) {
3646- return false
3647- }
3648- }
3649-
36503655 // If assistant message with completed ExitPlanMode, we found an unapproved plan
36513656 if ( msg . role === "assistant" && msg . parts ) {
36523657 const exitPlanPart = msg . parts . find (
@@ -3659,7 +3664,7 @@ const ChatViewInner = memo(function ChatViewInner({
36593664 }
36603665 }
36613666 return false
3662- } , [ messages ] )
3667+ } , [ messages , isPlanMode ] )
36633668
36643669 // Keep ref in sync for use in initializeScroll (which runs in useLayoutEffect)
36653670 hasUnapprovedPlanRef . current = hasUnapprovedPlan
@@ -4130,40 +4135,49 @@ export function ChatView({
41304135 [ chatId ] ,
41314136 )
41324137 const [ isTerminalSidebarOpen , setIsTerminalSidebarOpen ] = useAtom ( terminalSidebarAtom )
4133- const [ diffStats , setDiffStatsRaw ] = useState ( {
4134- fileCount : 0 ,
4135- additions : 0 ,
4136- deletions : 0 ,
4137- isLoading : true ,
4138- hasChanges : false ,
4139- } )
4140- // Smart setter that only updates if values actually changed
4138+
4139+ // Diff data cache - stored in atoms to persist across workspace switches
4140+ const diffCacheAtom = useMemo (
4141+ ( ) => workspaceDiffCacheAtomFamily ( chatId ) ,
4142+ [ chatId ] ,
4143+ )
4144+ const [ diffCache , setDiffCache ] = useAtom ( diffCacheAtom )
4145+
4146+ // Extract diff data from cache
4147+ const diffStats = diffCache . diffStats
4148+ const parsedFileDiffs = diffCache . parsedFileDiffs as ParsedDiffFile [ ] | null
4149+ const prefetchedFileContents = diffCache . prefetchedFileContents
4150+ const diffContent = diffCache . diffContent
4151+
4152+ // Smart setters that update the cache
41414153 const setDiffStats = useCallback ( ( val : any ) => {
4142- setDiffStatsRaw ( ( prev : typeof diffStats ) => {
4143- // Handle function updates
4144- const newVal = typeof val === 'function' ? val ( prev ) : val
4154+ setDiffCache ( ( prev ) => {
4155+ const newVal = typeof val === 'function' ? val ( prev . diffStats ) : val
41454156 // Only update if something changed
41464157 if (
4147- prev . fileCount === newVal . fileCount &&
4148- prev . additions === newVal . additions &&
4149- prev . deletions === newVal . deletions &&
4150- prev . isLoading === newVal . isLoading &&
4151- prev . hasChanges === newVal . hasChanges
4158+ prev . diffStats . fileCount === newVal . fileCount &&
4159+ prev . diffStats . additions === newVal . additions &&
4160+ prev . diffStats . deletions === newVal . deletions &&
4161+ prev . diffStats . isLoading === newVal . isLoading &&
4162+ prev . diffStats . hasChanges === newVal . hasChanges
41524163 ) {
41534164 return prev // Return same reference to prevent re-render
41544165 }
4155- return newVal
4166+ return { ... prev , diffStats : newVal }
41564167 } )
4157- } , [ ] )
4158- // Store raw diff content to pass to AgentDiffView (avoids double fetch)
4159- const [ diffContent , setDiffContent ] = useState < string | null > ( null )
4160- // Store pre-parsed file diffs (avoids double parsing in AgentDiffView)
4161- // Server returns extended type with fileLang, isNewFile, isDeletedFile
4162- const [ parsedFileDiffs , setParsedFileDiffs ] = useState < ParsedDiffFile [ ] | null > ( null )
4163- // Store prefetched file contents for instant diff view opening
4164- const [ prefetchedFileContents , setPrefetchedFileContents ] = useState <
4165- Record < string , string >
4166- > ( { } )
4168+ } , [ setDiffCache ] )
4169+
4170+ const setParsedFileDiffs = useCallback ( ( files : ParsedDiffFile [ ] | null ) => {
4171+ setDiffCache ( ( prev ) => ( { ...prev , parsedFileDiffs : files as any } ) )
4172+ } , [ setDiffCache ] )
4173+
4174+ const setPrefetchedFileContents = useCallback ( ( contents : Record < string , string > ) => {
4175+ setDiffCache ( ( prev ) => ( { ...prev , prefetchedFileContents : contents } ) )
4176+ } , [ setDiffCache ] )
4177+
4178+ const setDiffContent = useCallback ( ( content : string | null ) => {
4179+ setDiffCache ( ( prev ) => ( { ...prev , diffContent : content } ) )
4180+ } , [ setDiffCache ] )
41674181 const [ diffMode , setDiffMode ] = useAtom ( diffViewModeAtom )
41684182 const [ diffDisplayMode , setDiffDisplayMode ] = useAtom ( diffViewDisplayModeAtom )
41694183 const subChatsSidebarMode = useAtomValue ( agentsSubChatsSidebarModeAtom )
0 commit comments