@@ -736,45 +736,60 @@ describe("ClineProvider - Parallel Mode Support", () => {
736736 } )
737737
738738 describe ( "saveViewState" , ( ) => {
739- it ( "should update viewLocalState when saveViewState is called " , async ( ) => {
739+ it ( "should update viewLocalState and persist mode through registered viewStates " , async ( ) => {
740740 const provider = new ClineProvider ( mockContext , mockOutputChannel , "sidebar" , new ContextProxy ( mockContext ) )
741741
742742 const contextProxySpy = vi . spyOn ( provider . contextProxy , "setValue" )
743743 await ( provider as any ) . setViewStateId ( "stable-sidebar-view" )
744744
745745 await ( provider as any ) . saveViewState ( "mode" , "architect" )
746746
747- // Verify viewLocalState was updated
748747 expect ( ( provider as any ) . viewLocalState . mode ) . toBe ( "architect" )
749-
750- // saveViewState uses the stable per-view id, not the construction-order viewId suffix.
751- expect ( contextProxySpy ) . toHaveBeenCalledWith ( "__view_state_stable-sidebar-view_mode" , "architect" )
752- expect ( contextProxySpy ) . not . toHaveBeenCalledWith ( `__view_state_${ provider . viewId } _mode` , expect . anything ( ) )
748+ expect ( provider . contextProxy . getValue ( "viewStates" as any ) ) . toMatchObject ( {
749+ "stable-sidebar-view" : { mode : "architect" } ,
750+ } )
751+ expect ( contextProxySpy ) . toHaveBeenCalledWith (
752+ "viewStates" ,
753+ expect . objectContaining ( {
754+ "stable-sidebar-view" : expect . objectContaining ( {
755+ mode : "architect" ,
756+ updatedAt : expect . any ( Number ) ,
757+ } ) ,
758+ } ) ,
759+ )
760+ expect ( contextProxySpy ) . not . toHaveBeenCalledWith ( "__view_state_stable-sidebar-view_mode" , expect . anything ( ) )
753761
754762 await provider . dispose ( )
755763 } )
756764
757- it ( "should update viewLocalState for currentApiConfigName" , async ( ) => {
765+ it ( "should update viewLocalState and persist currentApiConfigName through registered viewStates " , async ( ) => {
758766 const provider = new ClineProvider ( mockContext , mockOutputChannel , "sidebar" , new ContextProxy ( mockContext ) )
759767
768+ await ( provider as any ) . setViewStateId ( "stable-sidebar-view" )
760769 await ( provider as any ) . saveViewState ( "currentApiConfigName" , "my-profile" )
761770
762771 expect ( ( provider as any ) . viewLocalState . currentApiConfigName ) . toBe ( "my-profile" )
772+ expect ( provider . contextProxy . getValue ( "viewStates" as any ) ) . toMatchObject ( {
773+ "stable-sidebar-view" : { currentApiConfigName : "my-profile" } ,
774+ } )
763775
764776 await provider . dispose ( )
765777 } )
766778
767- it ( "should update viewLocalState for apiConfiguration" , async ( ) => {
779+ it ( "should update viewLocalState for apiConfiguration without persisting provider settings or secrets " , async ( ) => {
768780 const provider = new ClineProvider ( mockContext , mockOutputChannel , "sidebar" , new ContextProxy ( mockContext ) )
769781
770782 const testApiConfig = {
771783 apiProvider : "openrouter" as const ,
772784 openRouterModelId : "claude-3.5-sonnet" ,
785+ openRouterApiKey : "secret-key" ,
773786 }
774787
788+ await ( provider as any ) . setViewStateId ( "stable-sidebar-view" )
775789 await ( provider as any ) . saveViewState ( "apiConfiguration" , testApiConfig )
776790
777791 expect ( ( provider as any ) . viewLocalState . apiConfiguration ) . toEqual ( testApiConfig )
792+ expect ( provider . contextProxy . getValue ( "viewStates" as any ) ) . toBeUndefined ( )
778793
779794 await provider . dispose ( )
780795 } )
@@ -823,15 +838,13 @@ describe("ClineProvider - Parallel Mode Support", () => {
823838 await provider . dispose ( )
824839 } )
825840
826- it ( "should update viewLocalState when stable per-view values are loaded manually " , async ( ) => {
841+ it ( "should restore mode and currentApiConfigName from hydrated viewStates after extension reload " , async ( ) => {
827842 const provider = new ClineProvider ( mockContext , mockOutputChannel , "sidebar" , new ContextProxy ( mockContext ) )
828843 const stableViewId = "stable-sidebar-view"
829844
830- await provider . contextProxy . setValue ( `__view_state_${ stableViewId } _mode` as any , "architect" )
831- await provider . contextProxy . setValue (
832- `__view_state_${ stableViewId } _currentApiConfigName` as any ,
833- "new-profile" ,
834- )
845+ await provider . contextProxy . setValue ( "viewStates" as any , {
846+ [ stableViewId ] : { mode : "architect" , currentApiConfigName : "new-profile" , updatedAt : 123 } ,
847+ } )
835848
836849 await ( provider as any ) . setViewStateId ( stableViewId )
837850
@@ -842,33 +855,52 @@ describe("ClineProvider - Parallel Mode Support", () => {
842855 await provider . dispose ( )
843856 } )
844857
845- it ( "should restore mode, current API config name, and API configuration from stable per-view state " , async ( ) => {
858+ it ( "should resolve API configuration from the persisted profile selection " , async ( ) => {
846859 const provider = new ClineProvider ( mockContext , mockOutputChannel , "editor" , new ContextProxy ( mockContext ) )
847860 const stableViewId = "stable-editor-tab-a"
848- const persistedApiConfiguration = {
849- apiProvider : "openrouter" as const ,
861+ const getProfileSpy = vi . spyOn ( provider . providerSettingsManager , "getProfile" ) . mockResolvedValue ( {
862+ name : "profile-a" ,
863+ id : "profile-a-id" ,
864+ apiProvider : "openrouter" ,
850865 openRouterModelId : "openrouter/anthropic/claude-sonnet-4" ,
851- }
866+ } as any )
852867
853- await provider . contextProxy . setValue ( `__view_state_${ stableViewId } _mode` as any , "architect" )
854- await provider . contextProxy . setValue (
855- `__view_state_${ stableViewId } _currentApiConfigName` as any ,
856- "profile-a" ,
857- )
858- await provider . contextProxy . setValue (
859- `__view_state_${ stableViewId } _apiConfiguration` as any ,
860- persistedApiConfiguration ,
861- )
868+ await provider . contextProxy . setValue ( "viewStates" as any , {
869+ [ stableViewId ] : { mode : "architect" , currentApiConfigName : "profile-a" , updatedAt : 123 } ,
870+ } )
862871 await provider . contextProxy . setValue ( "mode" as any , "debugger" )
863872 await provider . contextProxy . setValue ( "currentApiConfigName" as any , "profile-b" )
864873 await provider . contextProxy . setValue ( "apiConfiguration" as any , { apiProvider : "anthropic" } )
865874
866875 await ( provider as any ) . setViewStateId ( stableViewId )
867876 const state = await provider . getState ( )
868877
878+ expect ( getProfileSpy ) . toHaveBeenCalledWith ( { name : "profile-a" } )
869879 expect ( state . mode ) . toBe ( "architect" )
870880 expect ( state . currentApiConfigName ) . toBe ( "profile-a" )
871- expect ( state . apiConfiguration ) . toMatchObject ( persistedApiConfiguration )
881+ expect ( state . apiConfiguration ) . toMatchObject ( {
882+ apiProvider : "openrouter" ,
883+ openRouterModelId : "openrouter/anthropic/claude-sonnet-4" ,
884+ } )
885+
886+ await provider . dispose ( )
887+ } )
888+
889+ it ( "should not throw when a persisted profile selection cannot be resolved" , async ( ) => {
890+ const provider = new ClineProvider ( mockContext , mockOutputChannel , "editor" , new ContextProxy ( mockContext ) )
891+ const stableViewId = "stable-editor-tab-a"
892+ vi . spyOn ( provider . providerSettingsManager , "getProfile" ) . mockRejectedValue ( new Error ( "missing profile" ) )
893+
894+ await provider . contextProxy . setValue ( "viewStates" as any , {
895+ [ stableViewId ] : { mode : "architect" , currentApiConfigName : "deleted-profile" , updatedAt : 123 } ,
896+ } )
897+
898+ await expect ( ( provider as any ) . setViewStateId ( stableViewId ) ) . resolves . toBeUndefined ( )
899+ const state = await provider . getState ( )
900+
901+ expect ( state . mode ) . toBe ( "architect" )
902+ expect ( state . currentApiConfigName ) . toBe ( "deleted-profile" )
903+ expect ( state . apiConfiguration . apiProvider ) . toBe ( "anthropic" )
872904
873905 await provider . dispose ( )
874906 } )
@@ -891,6 +923,27 @@ describe("ClineProvider - Parallel Mode Support", () => {
891923 } )
892924 } )
893925
926+ describe ( "persisted view state pruning" , ( ) => {
927+ it ( "should keep the newest 50 persisted view states" , async ( ) => {
928+ const provider = new ClineProvider ( mockContext , mockOutputChannel , "sidebar" , new ContextProxy ( mockContext ) )
929+ const states = Object . fromEntries (
930+ Array . from ( { length : 55 } , ( _ , index ) => [
931+ `view-${ index } ` ,
932+ { mode : `mode-${ index } ` , updatedAt : index } ,
933+ ] ) ,
934+ )
935+
936+ const pruned = ( provider as any ) . prunePersistedViewStates ( states )
937+
938+ expect ( Object . keys ( pruned ) ) . toHaveLength ( 50 )
939+ expect ( pruned [ "view-54" ] ) . toBeDefined ( )
940+ expect ( pruned [ "view-5" ] ) . toBeDefined ( )
941+ expect ( pruned [ "view-4" ] ) . toBeUndefined ( )
942+
943+ await provider . dispose ( )
944+ } )
945+ } )
946+
894947 describe ( "getState merging" , ( ) => {
895948 it ( "should merge viewLocalState on top of global state" , async ( ) => {
896949 const provider = new ClineProvider ( mockContext , mockOutputChannel , "sidebar" , new ContextProxy ( mockContext ) )
0 commit comments